mirror of
https://github.com/fxm90/GradientLoadingBar.git
synced 2026-06-16 12:24:31 +00:00
♻️ :: Rewrite library using modern Swift features, following SPM conventions
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# .swiftformat
|
||||
# Created by Felix Mau (https://felix.hamburg)
|
||||
#
|
||||
|
||||
#
|
||||
# "Global" formatting options
|
||||
#
|
||||
# "Global" formatting options will be applied from the `SwiftConfigurationFiles` repository.
|
||||
# See https://github.com/fxm90/SwiftConfigurationFiles for further details.
|
||||
#
|
||||
|
||||
#
|
||||
# File options
|
||||
#
|
||||
# Note: Excluded paths are relative to the SwiftFormat configuration file.
|
||||
# Therefore we can't specify them in the configuration file from the `SwiftConfigurationFiles` repository.
|
||||
#
|
||||
--exclude Pods
|
||||
@@ -1,26 +0,0 @@
|
||||
#
|
||||
# .swiftlint.yml
|
||||
# Created by Felix Mau (https://felix.hamburg)
|
||||
#
|
||||
|
||||
opt_in_rules:
|
||||
- file_header
|
||||
|
||||
file_header:
|
||||
required_pattern: |
|
||||
\/\/
|
||||
\/\/ SWIFTLINT_CURRENT_FILENAME
|
||||
\/\/ (GradientLoadingBar|Example|ExampleTests|ExampleSnapshotTests)
|
||||
\/\/
|
||||
\/\/ Created by .*? on \d{2}\.\d{2}\.\d{2}\.
|
||||
\/\/ Copyright © \d{4} .*?\. All rights reserved\.
|
||||
\/\/
|
||||
|
||||
included:
|
||||
- ./
|
||||
- ../GradientLoadingBar
|
||||
|
||||
excluded:
|
||||
- Pods
|
||||
|
||||
parent_config: Pods/SwiftConfigurationFiles/.swiftlint.yml
|
||||
@@ -1,586 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 55;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
DC4E50A16A3B84900FBD1981 /* Pods_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12DDBD589B02F5D3C9C3AF69 /* Pods_Example.framework */; };
|
||||
DCA9833A27D68AD400D6EA30 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9833927D68AD400D6EA30 /* ExampleApp.swift */; };
|
||||
DCA9833E27D68AD600D6EA30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCA9833D27D68AD600D6EA30 /* Assets.xcassets */; };
|
||||
DCA9834127D68AD600D6EA30 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCA9834027D68AD600D6EA30 /* Preview Assets.xcassets */; };
|
||||
DCA9836927D68E3500D6EA30 /* StoryboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9836827D68E3500D6EA30 /* StoryboardView.swift */; };
|
||||
DCA9836C27D6905000D6EA30 /* EntryPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9836B27D6905000D6EA30 /* EntryPointView.swift */; };
|
||||
DCA9837127D6912300D6EA30 /* BasicExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9837027D6912300D6EA30 /* BasicExampleViewController.swift */; };
|
||||
DCA9837627D693BB00D6EA30 /* BasicExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCA9837527D693BB00D6EA30 /* BasicExample.storyboard */; };
|
||||
DCA9837827D6951C00D6EA30 /* SafeAreaExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9837727D6951C00D6EA30 /* SafeAreaExampleViewController.swift */; };
|
||||
DCA9837A27D6958500D6EA30 /* SafeAreaExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCA9837927D6958500D6EA30 /* SafeAreaExample.storyboard */; };
|
||||
DCA9838727D6A44700D6EA30 /* NavigationBarExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9838627D6A44700D6EA30 /* NavigationBarExampleViewController.swift */; };
|
||||
DCA9838927D6A55A00D6EA30 /* NavigationBarExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCA9838827D6A55A00D6EA30 /* NavigationBarExample.storyboard */; };
|
||||
DCA9838B27D6A74800D6EA30 /* SwiftUIExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCA9838A27D6A74800D6EA30 /* SwiftUIExampleView.swift */; };
|
||||
F50E4C7E28061655001232D3 /* AdvancedExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F50E4C7D28061655001232D3 /* AdvancedExample.storyboard */; };
|
||||
F50E4C8028061715001232D3 /* AdvancedExampleTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F50E4C7F28061715001232D3 /* AdvancedExampleTableViewController.swift */; };
|
||||
F5C7E8882809CDEE00036B8E /* CustomColorsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C7E8872809CDEE00036B8E /* CustomColorsTableViewCell.swift */; };
|
||||
F5C7E88A2809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C7E8892809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift */; };
|
||||
F5C7E88C2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C7E88B2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift */; };
|
||||
F5C7E88F2809CF1200036B8E /* BorderedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C7E88E2809CF1200036B8E /* BorderedButton.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
12DDBD589B02F5D3C9C3AF69 /* Pods_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3299012A62A2E4310D02CD78 /* Pods-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.release.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.release.xcconfig"; sourceTree = "<group>"; };
|
||||
DCA9833627D68AD400D6EA30 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DCA9833927D68AD400D6EA30 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = "<group>"; };
|
||||
DCA9833D27D68AD600D6EA30 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
DCA9834027D68AD600D6EA30 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
DCA9836827D68E3500D6EA30 /* StoryboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardView.swift; sourceTree = "<group>"; };
|
||||
DCA9836B27D6905000D6EA30 /* EntryPointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryPointView.swift; sourceTree = "<group>"; };
|
||||
DCA9837027D6912300D6EA30 /* BasicExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicExampleViewController.swift; sourceTree = "<group>"; };
|
||||
DCA9837527D693BB00D6EA30 /* BasicExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BasicExample.storyboard; sourceTree = "<group>"; };
|
||||
DCA9837727D6951C00D6EA30 /* SafeAreaExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeAreaExampleViewController.swift; sourceTree = "<group>"; };
|
||||
DCA9837927D6958500D6EA30 /* SafeAreaExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SafeAreaExample.storyboard; sourceTree = "<group>"; };
|
||||
DCA9838627D6A44700D6EA30 /* NavigationBarExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarExampleViewController.swift; sourceTree = "<group>"; };
|
||||
DCA9838827D6A55A00D6EA30 /* NavigationBarExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NavigationBarExample.storyboard; sourceTree = "<group>"; };
|
||||
DCA9838A27D6A74800D6EA30 /* SwiftUIExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExampleView.swift; sourceTree = "<group>"; };
|
||||
F50E4C7D28061655001232D3 /* AdvancedExample.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = AdvancedExample.storyboard; sourceTree = "<group>"; };
|
||||
F50E4C7F28061715001232D3 /* AdvancedExampleTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedExampleTableViewController.swift; sourceTree = "<group>"; };
|
||||
F5C7E8872809CDEE00036B8E /* CustomColorsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomColorsTableViewCell.swift; sourceTree = "<group>"; };
|
||||
F5C7E8892809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceBuilderSetupTableViewCell.swift; sourceTree = "<group>"; };
|
||||
F5C7E88B2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSuperviewTableViewCell.swift; sourceTree = "<group>"; };
|
||||
F5C7E88E2809CF1200036B8E /* BorderedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedButton.swift; sourceTree = "<group>"; };
|
||||
FAA1A12E84EBC1F48401EB10 /* Pods-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.debug.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
DCA9833327D68AD400D6EA30 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DC4E50A16A3B84900FBD1981 /* Pods_Example.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
33D2CE217238EAE7A5515634 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
12DDBD589B02F5D3C9C3AF69 /* Pods_Example.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A6AC3866473F1A669D221CA4 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FAA1A12E84EBC1F48401EB10 /* Pods-Example.debug.xcconfig */,
|
||||
3299012A62A2E4310D02CD78 /* Pods-Example.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9832D27D68AD400D6EA30 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9833827D68AD400D6EA30 /* Example */,
|
||||
DCA9833727D68AD400D6EA30 /* Products */,
|
||||
A6AC3866473F1A669D221CA4 /* Pods */,
|
||||
33D2CE217238EAE7A5515634 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9833727D68AD400D6EA30 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9833627D68AD400D6EA30 /* Example.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9833827D68AD400D6EA30 /* Example */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9836727D68E3500D6EA30 /* Helper */,
|
||||
DCA9836A27D68E3C00D6EA30 /* Scenes */,
|
||||
DCA9833927D68AD400D6EA30 /* ExampleApp.swift */,
|
||||
DCA9833D27D68AD600D6EA30 /* Assets.xcassets */,
|
||||
DCA9833F27D68AD600D6EA30 /* Preview Content */,
|
||||
);
|
||||
path = Example;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9833F27D68AD600D6EA30 /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9834027D68AD600D6EA30 /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9836727D68E3500D6EA30 /* Helper */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9836827D68E3500D6EA30 /* StoryboardView.swift */,
|
||||
);
|
||||
path = Helper;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9836A27D68E3C00D6EA30 /* Scenes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9836E27D6909A00D6EA30 /* AdvancedExample */,
|
||||
DCA9836D27D6908F00D6EA30 /* BasicExample */,
|
||||
DCA9837427D691C600D6EA30 /* NavigationBarExample */,
|
||||
DCA9837227D691A200D6EA30 /* SafeAreaExample */,
|
||||
DCA9836F27D690A400D6EA30 /* SwiftUIExample */,
|
||||
DCA9836B27D6905000D6EA30 /* EntryPointView.swift */,
|
||||
);
|
||||
path = Scenes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9836D27D6908F00D6EA30 /* BasicExample */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9837527D693BB00D6EA30 /* BasicExample.storyboard */,
|
||||
DCA9837027D6912300D6EA30 /* BasicExampleViewController.swift */,
|
||||
);
|
||||
path = BasicExample;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9836E27D6909A00D6EA30 /* AdvancedExample */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F5C7E8862809CDE200036B8E /* Cells */,
|
||||
F5C7E88D2809CF0700036B8E /* Views */,
|
||||
F50E4C7D28061655001232D3 /* AdvancedExample.storyboard */,
|
||||
F50E4C7F28061715001232D3 /* AdvancedExampleTableViewController.swift */,
|
||||
);
|
||||
path = AdvancedExample;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9836F27D690A400D6EA30 /* SwiftUIExample */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9838A27D6A74800D6EA30 /* SwiftUIExampleView.swift */,
|
||||
);
|
||||
path = SwiftUIExample;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9837227D691A200D6EA30 /* SafeAreaExample */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9837927D6958500D6EA30 /* SafeAreaExample.storyboard */,
|
||||
DCA9837727D6951C00D6EA30 /* SafeAreaExampleViewController.swift */,
|
||||
);
|
||||
path = SafeAreaExample;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DCA9837427D691C600D6EA30 /* NavigationBarExample */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DCA9838827D6A55A00D6EA30 /* NavigationBarExample.storyboard */,
|
||||
DCA9838627D6A44700D6EA30 /* NavigationBarExampleViewController.swift */,
|
||||
);
|
||||
path = NavigationBarExample;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F5C7E8862809CDE200036B8E /* Cells */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F5C7E8872809CDEE00036B8E /* CustomColorsTableViewCell.swift */,
|
||||
F5C7E8892809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift */,
|
||||
F5C7E88B2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift */,
|
||||
);
|
||||
path = Cells;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F5C7E88D2809CF0700036B8E /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F5C7E88E2809CF1200036B8E /* BorderedButton.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
DCA9833527D68AD400D6EA30 /* Example */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = DCA9835A27D68AD600D6EA30 /* Build configuration list for PBXNativeTarget "Example" */;
|
||||
buildPhases = (
|
||||
52082F0E9B4B579867A82146 /* [CP] Check Pods Manifest.lock */,
|
||||
DCA9833227D68AD400D6EA30 /* Sources */,
|
||||
DCA9833327D68AD400D6EA30 /* Frameworks */,
|
||||
DCA9833427D68AD400D6EA30 /* Resources */,
|
||||
C56CF067D28EB40785524B49 /* [CP] Embed Pods Frameworks */,
|
||||
DCA9836527D68D6400D6EA30 /* SwiftFormat */,
|
||||
DCA9836627D68D7000D6EA30 /* SwiftLint */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Example;
|
||||
productName = Example;
|
||||
productReference = DCA9833627D68AD400D6EA30 /* Example.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
DCA9832E27D68AD400D6EA30 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1320;
|
||||
LastUpgradeCheck = 1320;
|
||||
TargetAttributes = {
|
||||
DCA9833527D68AD400D6EA30 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = DCA9833127D68AD400D6EA30 /* Build configuration list for PBXProject "Example" */;
|
||||
compatibilityVersion = "Xcode 13.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = DCA9832D27D68AD400D6EA30;
|
||||
productRefGroup = DCA9833727D68AD400D6EA30 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
DCA9833527D68AD400D6EA30 /* Example */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
DCA9833427D68AD400D6EA30 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DCA9834127D68AD600D6EA30 /* Preview Assets.xcassets in Resources */,
|
||||
F50E4C7E28061655001232D3 /* AdvancedExample.storyboard in Resources */,
|
||||
DCA9837A27D6958500D6EA30 /* SafeAreaExample.storyboard in Resources */,
|
||||
DCA9837627D693BB00D6EA30 /* BasicExample.storyboard in Resources */,
|
||||
DCA9833E27D68AD600D6EA30 /* Assets.xcassets in Resources */,
|
||||
DCA9838927D6A55A00D6EA30 /* NavigationBarExample.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
52082F0E9B4B579867A82146 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Example-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C56CF067D28EB40785524B49 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
DCA9836527D68D6400D6EA30 /* SwiftFormat */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = SwiftFormat;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "SWIFTFORMAT=\"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat\"\nif [ ! -f \"$SWIFTFORMAT\" ]; then\n echo \"warning: SwiftFormat not installed!\"\n exit 1\nfi\n\n$SWIFTFORMAT ../ --config \"${PODS_ROOT}/SwiftConfigurationFiles/.swiftformat\"\n";
|
||||
};
|
||||
DCA9836627D68D7000D6EA30 /* SwiftLint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = SwiftLint;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "SWIFTLINT=\"${PODS_ROOT}/SwiftLint/swiftlint\"\nif [ ! -f \"$SWIFTLINT\" ]; then\n echo \"warning: SwiftLint not installed!\"\n exit 1\nfi\n\n$SWIFTLINT\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
DCA9833227D68AD400D6EA30 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DCA9837127D6912300D6EA30 /* BasicExampleViewController.swift in Sources */,
|
||||
DCA9833A27D68AD400D6EA30 /* ExampleApp.swift in Sources */,
|
||||
F5C7E88F2809CF1200036B8E /* BorderedButton.swift in Sources */,
|
||||
F5C7E8882809CDEE00036B8E /* CustomColorsTableViewCell.swift in Sources */,
|
||||
DCA9836927D68E3500D6EA30 /* StoryboardView.swift in Sources */,
|
||||
F5C7E88C2809CDFD00036B8E /* CustomSuperviewTableViewCell.swift in Sources */,
|
||||
F5C7E88A2809CDF600036B8E /* InterfaceBuilderSetupTableViewCell.swift in Sources */,
|
||||
F50E4C8028061715001232D3 /* AdvancedExampleTableViewController.swift in Sources */,
|
||||
DCA9836C27D6905000D6EA30 /* EntryPointView.swift in Sources */,
|
||||
DCA9837827D6951C00D6EA30 /* SafeAreaExampleViewController.swift in Sources */,
|
||||
DCA9838727D6A44700D6EA30 /* NavigationBarExampleViewController.swift in Sources */,
|
||||
DCA9838B27D6A74800D6EA30 /* SwiftUIExampleView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
DCA9835827D68AD600D6EA30 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
DCA9835927D68AD600D6EA30 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
DCA9835B27D68AD600D6EA30 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FAA1A12E84EBC1F48401EB10 /* Pods-Example.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = F63GJC2AN7;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.gradientLoadingBar.example.Example;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
DCA9835C27D68AD600D6EA30 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3299012A62A2E4310D02CD78 /* Pods-Example.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = F63GJC2AN7;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = hamburg.felix.gradientLoadingBar.example.Example;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
DCA9833127D68AD400D6EA30 /* Build configuration list for PBXProject "Example" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DCA9835827D68AD600D6EA30 /* Debug */,
|
||||
DCA9835927D68AD600D6EA30 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
DCA9835A27D68AD600D6EA30 /* Build configuration list for PBXNativeTarget "Example" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DCA9835B27D68AD600D6EA30 /* Debug */,
|
||||
DCA9835C27D68AD600D6EA30 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = DCA9832E27D68AD400D6EA30 /* Project object */;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Example.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
//
|
||||
// ExampleApp.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 07.03.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct ExampleApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
EntryPointView()
|
||||
.onAppear {
|
||||
// Use iOS 13 navigation bar appearance.
|
||||
//
|
||||
// While the entry point scene looks better with an opaque navigation background, the child scenes look
|
||||
// better with the default navigation background. As we can't update the appearance per scene we fallback
|
||||
// to an appearance with the default background here.
|
||||
//
|
||||
// Based on: https://sarunw.com/posts/uinavigationbar-changes-in-ios13/
|
||||
let appearance = UINavigationBarAppearance()
|
||||
appearance.configureWithDefaultBackground()
|
||||
|
||||
UINavigationBar.appearance().scrollEdgeAppearance = appearance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// StoryboardView.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 02.02.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StoryboardView: UIViewControllerRepresentable {
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
let name: String
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
func makeUIViewController(context _: Context) -> UIViewController {
|
||||
let storyboard = UIStoryboard(name: name, bundle: nil)
|
||||
guard let viewController = storyboard.instantiateInitialViewController() else {
|
||||
fatalError("⚠️ – Could not instantiate initial view controller for storyboard with name `\(name)`.")
|
||||
}
|
||||
|
||||
return viewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_: UIViewController, context _: Context) {}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ZAD-6Y-gxa">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Advanced Example Table View Controller-->
|
||||
<scene sceneID="iwG-zT-VrO">
|
||||
<objects>
|
||||
<tableViewController id="ZAD-6Y-gxa" customClass="AdvancedExampleTableViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" estimatedSectionHeaderHeight="-1" sectionFooterHeight="18" estimatedSectionFooterHeight="-1" id="2vt-bX-4b7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="CustomColorsTableViewCell" id="qq9-jr-nNk" customClass="CustomColorsTableViewCell" customModule="Example" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="49.5" width="374" height="120.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="qq9-jr-nNk" id="60G-w2-6BV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="120.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="zN2-pr-LT6">
|
||||
<rect key="frame" x="16" y="16" width="342" height="88.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Example showing how to provide custom gradient colors." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lbJ-pG-fGJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="342" height="36"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uL7-lu-aRP" userLabel="Toggle Custom Colors Button">
|
||||
<rect key="frame" x="0.0" y="48" width="342" height="40.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="jZd-Vw-MAb"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Toggle Custom Colors ↑">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="customColorsButtonTouchUpInside:" destination="qq9-jr-nNk" eventType="touchUpInside" id="XyI-4Q-sja"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="zN2-pr-LT6" firstAttribute="leading" secondItem="60G-w2-6BV" secondAttribute="leading" constant="16" id="18p-RW-IFT"/>
|
||||
<constraint firstAttribute="bottom" secondItem="zN2-pr-LT6" secondAttribute="bottom" constant="16" id="6LP-eX-o55"/>
|
||||
<constraint firstItem="zN2-pr-LT6" firstAttribute="top" secondItem="60G-w2-6BV" secondAttribute="top" constant="16" id="RzJ-mr-IHP"/>
|
||||
<constraint firstAttribute="trailing" secondItem="zN2-pr-LT6" secondAttribute="trailing" constant="16" id="h7A-pm-6hb"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="InterfaceBuilderSetupTableViewCell" id="yBS-fc-uJy" customClass="InterfaceBuilderSetupTableViewCell" customModule="Example" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="170" width="374" height="83.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="yBS-fc-uJy" id="Tid-am-qRM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="83.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="KTF-6b-NmT">
|
||||
<rect key="frame" x="16" y="16" width="342" height="51.5"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bCd-m6-FJm" customClass="GradientActivityIndicatorView" customModule="GradientLoadingBar">
|
||||
<rect key="frame" x="0.0" y="0.0" width="342" height="3"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="3" id="FQe-RY-KH7"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TpO-6A-9Bl" userLabel="Toggle IB Setup Button">
|
||||
<rect key="frame" x="0.0" y="11" width="342" height="40.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="QLO-3y-Y29"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Toggle IB Setup 🎨">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="toggleIBSetupButtonTouchUpInside:" destination="yBS-fc-uJy" eventType="touchUpInside" id="WLP-Dw-VmD"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="KTF-6b-NmT" firstAttribute="top" secondItem="Tid-am-qRM" secondAttribute="top" constant="16" id="2Bc-ex-Ggk"/>
|
||||
<constraint firstItem="KTF-6b-NmT" firstAttribute="leading" secondItem="Tid-am-qRM" secondAttribute="leading" constant="16" id="Aka-HV-cVb"/>
|
||||
<constraint firstAttribute="bottom" secondItem="KTF-6b-NmT" secondAttribute="bottom" constant="16" id="MH0-A6-FGH"/>
|
||||
<constraint firstAttribute="trailing" secondItem="KTF-6b-NmT" secondAttribute="trailing" constant="16" id="pLD-Mq-O5j"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="gradientActivityIndicator" destination="bCd-m6-FJm" id="z4t-IP-gqw"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="CustomSuperviewViewCell" id="ah2-WM-rFd" customClass="CustomSuperviewTableViewCell" customModule="Example" customModuleProvider="target">
|
||||
<rect key="frame" x="20" y="253.5" width="374" height="120.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ah2-WM-rFd" id="2Sj-GS-zEL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="120.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="ja4-s5-ZP7">
|
||||
<rect key="frame" x="20" y="16" width="334" height="88.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Example showing how to add the loading bar as a subview." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c7X-oS-Zcv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="334" height="36"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZxZ-rY-wUa" userLabel="Toggle Custom Superview Button" customClass="BorderedButton" customModule="Example" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="48" width="334" height="40.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="YS7-tA-cMK"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="plain" title="Toggle Custom Superview 🧪">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="toggleButtonTouchUpInside:" destination="ah2-WM-rFd" eventType="touchUpInside" id="W51-If-b5H"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ja4-s5-ZP7" firstAttribute="top" secondItem="2Sj-GS-zEL" secondAttribute="top" constant="16" id="ERr-8u-XhG"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ja4-s5-ZP7" secondAttribute="trailing" constant="20" id="SsK-Vx-1BL"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ja4-s5-ZP7" secondAttribute="bottom" constant="16" id="UBf-R5-ixV"/>
|
||||
<constraint firstItem="ja4-s5-ZP7" firstAttribute="leading" secondItem="2Sj-GS-zEL" secondAttribute="leading" constant="20" id="xgE-OJ-VIu"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="toggleButton" destination="ZxZ-rY-wUa" id="TwF-62-tM1"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="ZAD-6Y-gxa" id="3QD-qs-jpF"/>
|
||||
<outlet property="delegate" destination="ZAD-6Y-gxa" id="yIF-PG-bRO"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<extendedEdge key="edgesForExtendedLayout" bottom="YES"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="R94-H9-mvS" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-165" y="27"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<designables>
|
||||
<designable name="ZxZ-rY-wUa">
|
||||
<size key="intrinsicContentSize" width="216.5" height="31"/>
|
||||
</designable>
|
||||
</designables>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,136 +0,0 @@
|
||||
//
|
||||
// AdvancedExampleTableViewController.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 12.04.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import GradientLoadingBar
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
final class AdvancedExampleTableViewController: UITableViewController {
|
||||
|
||||
// MARK: - Config
|
||||
|
||||
private enum Config {
|
||||
/// The custom gradient colors we use.
|
||||
/// Source: https://color.adobe.com/Pink-Flamingo-color-theme-10343714/
|
||||
static let gradientColors = [
|
||||
#colorLiteral(red: 0.9490196078, green: 0.3215686275, blue: 0.431372549, alpha: 1), #colorLiteral(red: 0.9450980392, green: 0.4784313725, blue: 0.5921568627, alpha: 1), #colorLiteral(red: 0.9529411765, green: 0.737254902, blue: 0.7843137255, alpha: 1), #colorLiteral(red: 0.4274509804, green: 0.8666666667, blue: 0.9490196078, alpha: 1), #colorLiteral(red: 0.7568627451, green: 0.9411764706, blue: 0.9568627451, alpha: 1),
|
||||
]
|
||||
|
||||
static let firstSectionHeaderHeight: CGFloat = 48
|
||||
static let defaultSectionHeaderHeight: CGFloat = 32
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
private enum Section: CaseIterable {
|
||||
case customColors
|
||||
case interfaceBuilderSetup
|
||||
case customSuperview
|
||||
|
||||
var cellIdentifier: String {
|
||||
switch self {
|
||||
case .customColors:
|
||||
return CustomColorsTableViewCell.reuseIdentifier
|
||||
|
||||
case .interfaceBuilderSetup:
|
||||
return InterfaceBuilderSetupTableViewCell.reuseIdentifier
|
||||
|
||||
case .customSuperview:
|
||||
return CustomSuperviewTableViewCell.reuseIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
var sectionTitle: String {
|
||||
switch self {
|
||||
case .customColors:
|
||||
return "Custom Colors"
|
||||
|
||||
case .interfaceBuilderSetup:
|
||||
return "Interface Builder Setup"
|
||||
|
||||
case .customSuperview:
|
||||
return "Custom Superview"
|
||||
}
|
||||
}
|
||||
|
||||
var sectionHeaderHeight: CGFloat {
|
||||
switch self {
|
||||
case .customColors:
|
||||
return Config.firstSectionHeaderHeight
|
||||
|
||||
case .interfaceBuilderSetup, .customSuperview:
|
||||
return Config.defaultSectionHeaderHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
private let gradientLoadingBar = GradientLoadingBar()
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .systemGroupedBackground
|
||||
|
||||
gradientLoadingBar.gradientColors = Config.gradientColors
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
||||
override func numberOfSections(in _: UITableView) -> Int {
|
||||
Section.allCases.count
|
||||
}
|
||||
|
||||
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let section = Section.allCases[indexPath.section]
|
||||
switch section {
|
||||
case .customColors:
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: section.cellIdentifier, for: indexPath) as? CustomColorsTableViewCell else {
|
||||
fatalError("⚠️ – Invalid table view setup. Expected to have `CustomColorsTableViewCell` at this point!")
|
||||
}
|
||||
|
||||
cell.tapHandler = { [weak self] in
|
||||
if self?.gradientLoadingBar.isHidden == true {
|
||||
self?.gradientLoadingBar.fadeIn()
|
||||
} else {
|
||||
self?.gradientLoadingBar.fadeOut()
|
||||
}
|
||||
}
|
||||
|
||||
return cell
|
||||
|
||||
case .interfaceBuilderSetup, .customSuperview:
|
||||
return tableView.dequeueReusableCell(withIdentifier: section.cellIdentifier, for: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
Section.allCases[section].sectionTitle
|
||||
}
|
||||
|
||||
override func tableView(_: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
Section.allCases[section].sectionHeaderHeight
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
struct AdvancedExampleView: View {
|
||||
var body: some View {
|
||||
StoryboardView(name: "AdvancedExample")
|
||||
.navigationTitle("🚀 Advanced Example")
|
||||
.edgesIgnoringSafeArea(.bottom)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// CustomColorsTableViewCell.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 15.04.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class CustomColorsTableViewCell: UITableViewCell {
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
static let reuseIdentifier = "CustomColorsTableViewCell"
|
||||
|
||||
var tapHandler: (() -> Void)?
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
@IBAction private func customColorsButtonTouchUpInside(_: Any) {
|
||||
tapHandler?()
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// CustomSuperviewTableViewCell.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 15.04.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import GradientLoadingBar
|
||||
import UIKit
|
||||
|
||||
final class CustomSuperviewTableViewCell: UITableViewCell {
|
||||
|
||||
// MARK: - Config
|
||||
|
||||
private enum Config {
|
||||
static let height: CGFloat = 3
|
||||
}
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
@IBOutlet private var toggleButton: UIButton!
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
static let reuseIdentifier = "CustomSuperviewViewCell"
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
private let gradientActivityIndicatorView = GradientActivityIndicatorView()
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
setupGradientActivityIndicatorView()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func setupGradientActivityIndicatorView() {
|
||||
gradientActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
toggleButton.addSubview(gradientActivityIndicatorView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
gradientActivityIndicatorView.leadingAnchor.constraint(equalTo: toggleButton.leadingAnchor),
|
||||
gradientActivityIndicatorView.trailingAnchor.constraint(equalTo: toggleButton.trailingAnchor),
|
||||
|
||||
gradientActivityIndicatorView.bottomAnchor.constraint(equalTo: toggleButton.bottomAnchor),
|
||||
gradientActivityIndicatorView.heightAnchor.constraint(equalToConstant: Config.height),
|
||||
])
|
||||
}
|
||||
|
||||
@IBAction private func toggleButtonTouchUpInside(_: Any) {
|
||||
if gradientActivityIndicatorView.isHidden {
|
||||
gradientActivityIndicatorView.fadeIn()
|
||||
} else {
|
||||
gradientActivityIndicatorView.fadeOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// InterfaceBuilderSetupTableViewCell.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 15.04.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import GradientLoadingBar
|
||||
import UIKit
|
||||
|
||||
final class InterfaceBuilderSetupTableViewCell: UITableViewCell {
|
||||
|
||||
// MARK: - Config
|
||||
|
||||
private enum Config {
|
||||
static let animationDuration: TimeInterval = 0.5
|
||||
}
|
||||
|
||||
// MARK: - Outlets
|
||||
|
||||
@IBOutlet private var gradientActivityIndicator: GradientActivityIndicatorView!
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
static let reuseIdentifier = "InterfaceBuilderSetupTableViewCell"
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
@IBAction private func toggleIBSetupButtonTouchUpInside(_: Any) {
|
||||
// We explicitly "only" reduce the alpha here, as calling `fadeIn()` / `fadeOut()` would update the `isHidden`
|
||||
// flag accordingly. This would then lead to a height-update of the parent stack view.
|
||||
UIView.animate(withDuration: Config.animationDuration) {
|
||||
if self.gradientActivityIndicator.alpha > 0 {
|
||||
self.gradientActivityIndicator.alpha = 0
|
||||
} else {
|
||||
self.gradientActivityIndicator.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//
|
||||
// BorderedButton.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 15.04.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@IBDesignable
|
||||
final class BorderedButton: UIButton {
|
||||
|
||||
// MARK: - Config
|
||||
|
||||
private enum Config {
|
||||
static let borderColor = #colorLiteral(red: 0.2862745098, green: 0.5647058824, blue: 0.8862745098, alpha: 1)
|
||||
static let borderWidth: CGFloat = 1
|
||||
static let cornerRadius: CGFloat = 4
|
||||
}
|
||||
|
||||
// MARK: - Instance Lifecycle
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
commonInit()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func commonInit() {
|
||||
backgroundColor = .clear
|
||||
tintColor = Config.borderColor
|
||||
|
||||
layer.borderColor = Config.borderColor.cgColor
|
||||
layer.borderWidth = Config.borderWidth
|
||||
layer.cornerRadius = Config.cornerRadius
|
||||
layer.masksToBounds = true
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Basic Example View Controller-->
|
||||
<scene sceneID="s0d-6b-0kx">
|
||||
<objects>
|
||||
<viewController id="Y6W-OH-hqX" customClass="BasicExampleViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="i3m-u5-CBZ" userLabel="Content Stack View">
|
||||
<rect key="frame" x="16" y="428" width="382" height="40"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iqR-B3-IAJ" userLabel="ShowButton">
|
||||
<rect key="frame" x="0.0" y="0.0" width="183" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="ygd-aW-5tx"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Show ↑">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="showButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="syE-bX-c9J"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0mi-83-2Nx" userLabel="HideButton">
|
||||
<rect key="frame" x="199" y="0.0" width="183" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="bWt-eh-ohc"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Hide ">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="hideButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="xWJ-X0-hCH"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="i3m-u5-CBZ" firstAttribute="centerY" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" id="J8f-pR-dOH"/>
|
||||
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="i3m-u5-CBZ" secondAttribute="trailing" constant="16" id="RpI-X5-afT"/>
|
||||
<constraint firstItem="i3m-u5-CBZ" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="qcn-hk-qmQ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="132" y="115"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// BasicExampleViewController.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 07.03.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import GradientLoadingBar
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
final class BasicExampleViewController: UIViewController {
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Reset any possible visible loading bar.
|
||||
GradientLoadingBar.shared.fadeOut()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
@IBAction private func showButtonTouchUpInside(_: Any) {
|
||||
GradientLoadingBar.shared.fadeIn()
|
||||
}
|
||||
|
||||
@IBAction private func hideButtonTouchUpInside(_: Any) {
|
||||
GradientLoadingBar.shared.fadeOut()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
struct BasicExampleView: View {
|
||||
var body: some View {
|
||||
StoryboardView(name: "BasicExample")
|
||||
.navigationTitle("🏡 Basic Example")
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
//
|
||||
// EntryPointView.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 07.03.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EntryPointView: View {
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
@State
|
||||
private var isNavigationBarExampleVisible = false
|
||||
|
||||
// MARK: - Render
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
Section(header: Text("UIKit Examples")) {
|
||||
NavigationLink(destination: BasicExampleView()) {
|
||||
TitleSubtitleView(title: "🏡 Basic Example",
|
||||
subtitle: "Basic usage and setup.")
|
||||
}
|
||||
|
||||
NavigationLink(destination: SafeAreaExampleView()) {
|
||||
TitleSubtitleView(title: "📲 Safe Area Example",
|
||||
subtitle: "Loading bar ignoring the safe area.")
|
||||
}
|
||||
|
||||
NavigationLink(destination: AdvancedExampleView()) {
|
||||
TitleSubtitleView(title: "🚀 Advanced Example",
|
||||
subtitle: "How to apply e.g. custom colors.")
|
||||
}
|
||||
|
||||
Button {
|
||||
isNavigationBarExampleVisible = true
|
||||
} label: {
|
||||
TitleSubtitleView(title: "🧭 Navigation Bar Example",
|
||||
subtitle: "Attach the loading bar to a navigation bar.")
|
||||
// Make entire row tappable and not just the text.
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
Section(header: Text("SwiftUI Examples")) {
|
||||
NavigationLink(destination: SwiftUIExampleView()) {
|
||||
TitleSubtitleView(title: "🎨 GradientLoadingBarView Example",
|
||||
subtitle: "How to use the SwiftUI view.")
|
||||
}
|
||||
}
|
||||
}
|
||||
// We present the `NavigationBarExampleView` as a sheet using it's own navigation controller.
|
||||
// The `NavigationView` passed from SwiftUI isn't accessible from a `UIViewController` in a way, that we can add subviews to it.
|
||||
.sheet(isPresented: $isNavigationBarExampleVisible) {
|
||||
NavigationBarExampleView()
|
||||
}
|
||||
// Unfortunately setting the title here results in constraint warnings.
|
||||
// I couldn't find a possible fix yet, even `.navigationViewStyle(.stack)` doesn't seem to work.
|
||||
// https://stackoverflow.com/q/65316497
|
||||
.navigationTitle("GradientLoadingBar")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
private struct TitleSubtitleView: View {
|
||||
let title: String
|
||||
let subtitle: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
struct EntryPointView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EntryPointView()
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="QqU-dK-9N3">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--🧭 Navigation Bar Example-->
|
||||
<scene sceneID="s0d-6b-0kx">
|
||||
<objects>
|
||||
<viewController id="Y6W-OH-hqX" customClass="NavigationBarExampleViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="NLc-ox-i4v">
|
||||
<rect key="frame" x="16" y="428" width="382" height="40"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="m50-Bi-edx" userLabel="ShowButton">
|
||||
<rect key="frame" x="0.0" y="0.0" width="183" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="3OT-yv-cyC"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Show">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="showButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="Wp7-QY-9dg"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aXh-Qp-yVs" userLabel="HideButton">
|
||||
<rect key="frame" x="199" y="0.0" width="183" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="0sc-bb-ZEH"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Hide">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="hideButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="vxz-29-LMX"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="NLc-ox-i4v" secondAttribute="trailing" constant="16" id="2Ig-gz-o5i"/>
|
||||
<constraint firstItem="NLc-ox-i4v" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="5DU-9Z-LFC"/>
|
||||
<constraint firstItem="NLc-ox-i4v" firstAttribute="centerY" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" id="uG6-3e-Y6q"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="🧭 Navigation Bar Example" id="cJ0-SC-qx4"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1042.0289855072465" y="61.607142857142854"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="Fzj-P1-1gQ">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="QqU-dK-9N3" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="Gew-of-pOh">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="Y6W-OH-hqX" kind="relationship" relationship="rootViewController" id="YmV-f4-HGX"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="brr-zi-WNx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="131.8840579710145" y="61.607142857142854"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,80 +0,0 @@
|
||||
//
|
||||
// NavigationBarExampleViewController.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 07.03.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import GradientLoadingBar
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
final class NavigationBarExampleViewController: UIViewController {
|
||||
|
||||
// MARK: - Config
|
||||
|
||||
private enum Config {
|
||||
/// The programatically applied height of the `GradientActivityIndicatorView`.
|
||||
static let height: CGFloat = 3
|
||||
}
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
private let gradientProgressIndicatorView = GradientActivityIndicatorView()
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let barButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
|
||||
navigationItem.rightBarButtonItem = barButtonItem
|
||||
|
||||
setupGradientProgressIndicatorView()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Reset any possible visible loading bar.
|
||||
gradientProgressIndicatorView.fadeOut()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
private func setupGradientProgressIndicatorView() {
|
||||
guard let navigationBar = navigationController?.navigationBar else { return }
|
||||
|
||||
gradientProgressIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationBar.addSubview(gradientProgressIndicatorView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
gradientProgressIndicatorView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor),
|
||||
gradientProgressIndicatorView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
|
||||
|
||||
gradientProgressIndicatorView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
|
||||
gradientProgressIndicatorView.heightAnchor.constraint(equalToConstant: Config.height),
|
||||
])
|
||||
}
|
||||
|
||||
@IBAction private func doneButtonTapped(_: Any) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
@IBAction private func showButtonTouchUpInside(_: Any) {
|
||||
gradientProgressIndicatorView.fadeIn()
|
||||
}
|
||||
|
||||
@IBAction private func hideButtonTouchUpInside(_: Any) {
|
||||
gradientProgressIndicatorView.fadeOut()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
struct NavigationBarExampleView: View {
|
||||
var body: some View {
|
||||
StoryboardView(name: "NavigationBarExample")
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Safe Area Example View Controller-->
|
||||
<scene sceneID="s0d-6b-0kx">
|
||||
<objects>
|
||||
<viewController id="Y6W-OH-hqX" customClass="SafeAreaExampleViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="vQo-n2-n4V" userLabel="Top Content Stack View">
|
||||
<rect key="frame" x="16" y="400" width="382" height="40"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xx7-io-IHi" userLabel="ShowBasicBarButton">
|
||||
<rect key="frame" x="0.0" y="0.0" width="244" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="wri-XG-tfO"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Show Basic Bar">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="showBasicBarButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="o8j-Z6-6kZ"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Xxs-Xn-l65" userLabel="HideBasicBarButton">
|
||||
<rect key="frame" x="260" y="0.0" width="122" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="zfx-4A-wZB"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Hide">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="hideBasicBarButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="XXK-yZ-LR4"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" alignment="center" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="J6X-XZ-YZb" userLabel="Bottom Content Stack View">
|
||||
<rect key="frame" x="16" y="456" width="382" height="40"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DS9-JT-RIl" userLabel="HideNotchBarButton">
|
||||
<rect key="frame" x="0.0" y="0.0" width="104" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="64O-UM-b99"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Hide">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="hideNotchBarButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="INf-lf-Egt"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8wt-vv-Sj1" userLabel="ShowNotchBarButton">
|
||||
<rect key="frame" x="120" y="0.0" width="262" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="40" id="nUb-HA-Cts"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="filled" title="Show Notch Bar">
|
||||
<fontDescription key="titleFontDescription" style="UICTFontTextStyleBody"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="showNotchBarButtonTouchUpInside:" destination="Y6W-OH-hqX" eventType="touchUpInside" id="aHn-BO-ovO"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="J6X-XZ-YZb" secondAttribute="trailing" constant="16" id="FLY-Rz-mWy"/>
|
||||
<constraint firstItem="vQo-n2-n4V" firstAttribute="leading" secondItem="5EZ-qb-Rvc" secondAttribute="leading" constant="16" id="Vh8-QH-gYf"/>
|
||||
<constraint firstItem="J6X-XZ-YZb" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" constant="8" id="XPa-0S-CCv"/>
|
||||
<constraint firstItem="J6X-XZ-YZb" firstAttribute="leading" secondItem="vDu-zF-Fre" secondAttribute="leading" constant="16" id="XpT-4k-VAf"/>
|
||||
<constraint firstItem="vQo-n2-n4V" firstAttribute="bottom" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" constant="-8" id="czm-UN-XEM"/>
|
||||
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="vQo-n2-n4V" secondAttribute="trailing" constant="16" id="umO-We-0lr"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="133" y="115"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// SafeAreaExampleViewController.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 07.03.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import GradientLoadingBar
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
final class SafeAreaExampleViewController: UIViewController {
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
private let gradientLoadingBar = GradientLoadingBar(isRelativeToSafeArea: false)
|
||||
private let notchGradientLoadingBar = NotchGradientLoadingBar(isRelativeToSafeArea: false)
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Reset any possible visible loading bar.
|
||||
gradientLoadingBar.fadeOut()
|
||||
notchGradientLoadingBar.fadeOut()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
|
||||
@IBAction private func showBasicBarButtonTouchUpInside(_: Any) {
|
||||
gradientLoadingBar.fadeIn()
|
||||
}
|
||||
|
||||
@IBAction private func hideBasicBarButtonTouchUpInside(_: Any) {
|
||||
gradientLoadingBar.fadeOut()
|
||||
}
|
||||
|
||||
@IBAction private func showNotchBarButtonTouchUpInside(_: Any) {
|
||||
notchGradientLoadingBar.fadeIn()
|
||||
}
|
||||
|
||||
@IBAction private func hideNotchBarButtonTouchUpInside(_: Any) {
|
||||
notchGradientLoadingBar.fadeOut()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
struct SafeAreaExampleView: View {
|
||||
var body: some View {
|
||||
StoryboardView(name: "SafeAreaExample")
|
||||
.navigationTitle("📲 Safe Area Example")
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
//
|
||||
// SwiftUIExampleView.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Mau on 07.03.22.
|
||||
// Copyright © 2022 Felix Mau. All rights reserved.
|
||||
//
|
||||
|
||||
import GradientLoadingBar
|
||||
import SwiftUI
|
||||
|
||||
struct SwiftUIExampleView: View {
|
||||
|
||||
// MARK: - Render
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Default example")) {
|
||||
DefaultExampleView()
|
||||
}
|
||||
|
||||
Section(header: Text("Custom colors")) {
|
||||
CustomColorsExampleView()
|
||||
}
|
||||
|
||||
Section(header: Text("Custom animation duration")) {
|
||||
CustomProgressDurationExampleView()
|
||||
}
|
||||
}
|
||||
.navigationTitle("🎨 SwiftUI Example")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
private struct DefaultExampleView: View {
|
||||
|
||||
@State
|
||||
private var isVisible = true
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text("Example using the default configuration.")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
GradientLoadingBarView()
|
||||
.frame(maxWidth: .infinity, maxHeight: 3)
|
||||
.cornerRadius(1.5)
|
||||
.opacity(isVisible ? 1 : 0)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Button("Show") {
|
||||
withAnimation(.easeInOut) {
|
||||
isVisible = true
|
||||
}
|
||||
}
|
||||
.buttonStyle(RoundedRectangleButtonStyle())
|
||||
|
||||
Button("Hide") {
|
||||
withAnimation(.easeInOut) {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
.buttonStyle(RoundedRectangleButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
}
|
||||
|
||||
private struct CustomColorsExampleView: View {
|
||||
|
||||
private enum Config {
|
||||
/// The custom gradient colors we use.
|
||||
/// Source: https://color.adobe.com/Pink-Flamingo-color-theme-10343714/
|
||||
static let gradientColors = [
|
||||
#colorLiteral(red: 0.9490196078, green: 0.3215686275, blue: 0.431372549, alpha: 1), #colorLiteral(red: 0.9450980392, green: 0.4784313725, blue: 0.5921568627, alpha: 1), #colorLiteral(red: 0.9529411765, green: 0.737254902, blue: 0.7843137255, alpha: 1), #colorLiteral(red: 0.4274509804, green: 0.8666666667, blue: 0.9490196078, alpha: 1), #colorLiteral(red: 0.7568627451, green: 0.9411764706, blue: 0.9568627451, alpha: 1),
|
||||
].map(Color.init)
|
||||
}
|
||||
|
||||
@State
|
||||
private var isVisible = true
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text("Example showing how to provide custom gradient colors.")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
GradientLoadingBarView(gradientColors: Config.gradientColors)
|
||||
.frame(maxWidth: .infinity, maxHeight: 3)
|
||||
.cornerRadius(1.5)
|
||||
.opacity(isVisible ? 1 : 0)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Button("Show") {
|
||||
withAnimation(.easeInOut) {
|
||||
isVisible = true
|
||||
}
|
||||
}
|
||||
.buttonStyle(RoundedRectangleButtonStyle())
|
||||
|
||||
Button("Hide") {
|
||||
withAnimation(.easeInOut) {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
.buttonStyle(RoundedRectangleButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
}
|
||||
|
||||
private struct CustomProgressDurationExampleView: View {
|
||||
|
||||
private enum Config {
|
||||
/// The custom progress duration to use.
|
||||
static let progressDuration: TimeInterval = 10
|
||||
}
|
||||
|
||||
@State
|
||||
private var isVisible = true
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text("Example showing how to provide a custom progress animation duration.")
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
GradientLoadingBarView(progressDuration: Config.progressDuration)
|
||||
.frame(maxWidth: .infinity, maxHeight: 3)
|
||||
.cornerRadius(1.5)
|
||||
.opacity(isVisible ? 1 : 0)
|
||||
|
||||
HStack(spacing: 16) {
|
||||
Button("Show") {
|
||||
withAnimation(.easeInOut) {
|
||||
isVisible = true
|
||||
}
|
||||
}
|
||||
.buttonStyle(RoundedRectangleButtonStyle())
|
||||
|
||||
Button("Hide") {
|
||||
withAnimation(.easeInOut) {
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
.buttonStyle(RoundedRectangleButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 16)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private struct RoundedRectangleButtonStyle: ButtonStyle {
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundColor(.white)
|
||||
.background(Color.blue.cornerRadius(8))
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
struct SwiftUIExampleView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SwiftUIExampleView()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "cocoapods"
|
||||
gem "fastlane"
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
@@ -1,286 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
activesupport (6.1.6.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.1)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.626.0)
|
||||
aws-sdk-core (3.141.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.58.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.11.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.11.3)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.4.0, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.4.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 1.0, < 3.0)
|
||||
xcodeproj (>= 1.21.0, < 2.0)
|
||||
cocoapods-core (1.11.3)
|
||||
activesupport (>= 5.0, < 7)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
netrc (~> 0.11)
|
||||
public_suffix (~> 4.0)
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.5)
|
||||
cocoapods-downloader (1.6.3)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.1)
|
||||
cocoapods-trunk (1.6.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
concurrent-ruby (1.1.10)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.4)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ffi (>= 1.15.0)
|
||||
excon (0.92.4)
|
||||
faraday (1.10.2)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.209.1)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (~> 0.1.1)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fastlane-plugin-swiftformat (0.3.0)
|
||||
ffi (1.15.5)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.25.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-core (0.7.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.13.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.10.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-storage_v1 (0.17.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.39.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.17.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.2.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.2)
|
||||
jwt (2.5.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.16.3)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
netrc (0.11.0)
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
public_suffix (4.0.7)
|
||||
rake (13.0.6)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rouge (2.0.7)
|
||||
ruby-macho (2.5.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.17.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
zeitwerk (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods
|
||||
fastlane
|
||||
fastlane-plugin-swiftformat
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.18
|
||||
@@ -1,45 +0,0 @@
|
||||
# Fixes warning `Your project does not explicitly specify the CocoaPods master specs repo`.
|
||||
source 'https://github.com/CocoaPods/Specs.git'
|
||||
|
||||
platform :ios, '13.0'
|
||||
use_frameworks!
|
||||
|
||||
target 'Example' do
|
||||
pod 'GradientLoadingBar', :path => '../', :testspecs => ['Tests']
|
||||
|
||||
# Development pods.
|
||||
pod 'SwiftFormat/CLI', '~> 0.41'
|
||||
pod 'SwiftLint', '~> 0.42'
|
||||
pod 'SwiftConfigurationFiles', :git => 'https://github.com/fxm90/SwiftConfigurationFiles.git'
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if target.name == 'GradientLoadingBar'
|
||||
# Explicitly set the iOS deployment target for `GradientLoadingBar` in our pods project, as this will be read by Carthage.
|
||||
#
|
||||
# - Note: This value has to match `s.ios.deployment_target` from the `.podspec` file!!
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
|
||||
else
|
||||
# Remove the deployment target from all other pods in our project and let them inherit
|
||||
# the project/workspace deployment target that has been specified at the top of the Podfile.
|
||||
# Source: https://stackoverflow.com/a/63489366
|
||||
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Fix issues with interface builder and CocoaPods.
|
||||
#
|
||||
# Source: https://github.com/CocoaPods/CocoaPods/issues/7606#issuecomment-484294739
|
||||
#
|
||||
installer.pods_project.build_configurations.each do |config|
|
||||
next unless config.name == 'Debug'
|
||||
|
||||
config.build_settings['LD_RUNPATH_SEARCH_PATHS'] = [
|
||||
'$(FRAMEWORK_SEARCH_PATHS)'
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,43 +0,0 @@
|
||||
PODS:
|
||||
- GradientLoadingBar (3.0.0)
|
||||
- GradientLoadingBar/Tests (3.0.0):
|
||||
- SnapshotTesting (~> 1.9)
|
||||
- SnapshotTesting (1.9.0)
|
||||
- SwiftConfigurationFiles (0.0.1)
|
||||
- SwiftFormat/CLI (0.50.1)
|
||||
- SwiftLint (0.49.1)
|
||||
|
||||
DEPENDENCIES:
|
||||
- GradientLoadingBar (from `../`)
|
||||
- GradientLoadingBar/Tests (from `../`)
|
||||
- SwiftConfigurationFiles (from `https://github.com/fxm90/SwiftConfigurationFiles.git`)
|
||||
- SwiftFormat/CLI (~> 0.41)
|
||||
- SwiftLint (~> 0.42)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/CocoaPods/Specs.git:
|
||||
- SnapshotTesting
|
||||
- SwiftFormat
|
||||
- SwiftLint
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
GradientLoadingBar:
|
||||
:path: "../"
|
||||
SwiftConfigurationFiles:
|
||||
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
SwiftConfigurationFiles:
|
||||
:commit: efe01ba39069e8957fb89217b8c7fed298254691
|
||||
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
GradientLoadingBar: 3c4246535efdedaf9b471c05caabfdb4b87b40a2
|
||||
SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b
|
||||
SwiftConfigurationFiles: 1cf2228a911ebed9f42f8dec077bb634f04ca6c8
|
||||
SwiftFormat: e73212c71908404e333da34e303772b9e516ac9b
|
||||
SwiftLint: 32ee33ded0636d0905ef6911b2b67bbaeeedafa5
|
||||
|
||||
PODFILE CHECKSUM: ea7965c23e5364bcd5cd7d3693d685af73a3e31d
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
Generated
-46
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" systemVersion="17A277" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -1,30 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CPTestAppHostAppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIWindow *window;
|
||||
|
||||
@end
|
||||
|
||||
@implementation CPTestAppHostAppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)__unused application didFinishLaunchingWithOptions:(NSDictionary *)__unused launchOptions
|
||||
{
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
self.window.rootViewController = [UIViewController new];
|
||||
|
||||
[self.window makeKeyAndVisible];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
@autoreleasepool
|
||||
{
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([CPTestAppHostAppDelegate class]));
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "GradientLoadingBar",
|
||||
"version": "3.0.0",
|
||||
"summary": "A customizable animated gradient loading bar.",
|
||||
"description": "A customizable animated gradient loading bar.\nInspired by https://codepen.io/marcobiedermann/pen/LExXWW",
|
||||
"homepage": "https://github.com/fxm90/GradientLoadingBar",
|
||||
"screenshots": "https://raw.githubusercontent.com/fxm90/GradientLoadingBar/master/Assets/screen.gif",
|
||||
"license": {
|
||||
"type": "MIT",
|
||||
"file": "LICENSE"
|
||||
},
|
||||
"authors": {
|
||||
"Felix Mau": "contact@felix.hamburg"
|
||||
},
|
||||
"source": {
|
||||
"git": "https://github.com/fxm90/GradientLoadingBar.git",
|
||||
"tag": "3.0.0"
|
||||
},
|
||||
"swift_versions": "5.5",
|
||||
"platforms": {
|
||||
"ios": "13.0"
|
||||
},
|
||||
"source_files": "GradientLoadingBar/Sources/**/*.swift",
|
||||
"testspecs": [
|
||||
{
|
||||
"name": "Tests",
|
||||
"test_type": "unit",
|
||||
"requires_app_host": true,
|
||||
"source_files": "GradientLoadingBar/Tests/**/*.{swift,md}",
|
||||
"dependencies": {
|
||||
"SnapshotTesting": [
|
||||
"~> 1.9"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"swift_version": "5.5"
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "SwiftConfigurationFiles",
|
||||
"version": "0.0.1",
|
||||
"summary": "Repository containing configuration files for Swift development, e.g. for tools like SwiftLint or SwiftFormat.",
|
||||
"homepage": "https://github.com/fxm90/Swift-Configuration-Files",
|
||||
"license": {
|
||||
"type": "MIT",
|
||||
"file": "LICENSE"
|
||||
},
|
||||
"authors": {
|
||||
"Felix Mau": "me@felix.hamburg"
|
||||
},
|
||||
"source": {
|
||||
"git": "https://github.com/fxm90/Swift-Configuration-Files.git",
|
||||
"tag": "0.0.1"
|
||||
},
|
||||
"social_media_url": "https://twitter.com/_fxm90",
|
||||
"preserve_paths": ".*",
|
||||
"platforms": {
|
||||
"osx": null,
|
||||
"ios": null,
|
||||
"tvos": null,
|
||||
"watchos": null
|
||||
}
|
||||
}
|
||||
Generated
-43
@@ -1,43 +0,0 @@
|
||||
PODS:
|
||||
- GradientLoadingBar (3.0.0)
|
||||
- GradientLoadingBar/Tests (3.0.0):
|
||||
- SnapshotTesting (~> 1.9)
|
||||
- SnapshotTesting (1.9.0)
|
||||
- SwiftConfigurationFiles (0.0.1)
|
||||
- SwiftFormat/CLI (0.50.1)
|
||||
- SwiftLint (0.49.1)
|
||||
|
||||
DEPENDENCIES:
|
||||
- GradientLoadingBar (from `../`)
|
||||
- GradientLoadingBar/Tests (from `../`)
|
||||
- SwiftConfigurationFiles (from `https://github.com/fxm90/SwiftConfigurationFiles.git`)
|
||||
- SwiftFormat/CLI (~> 0.41)
|
||||
- SwiftLint (~> 0.42)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/CocoaPods/Specs.git:
|
||||
- SnapshotTesting
|
||||
- SwiftFormat
|
||||
- SwiftLint
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
GradientLoadingBar:
|
||||
:path: "../"
|
||||
SwiftConfigurationFiles:
|
||||
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
SwiftConfigurationFiles:
|
||||
:commit: efe01ba39069e8957fb89217b8c7fed298254691
|
||||
:git: https://github.com/fxm90/SwiftConfigurationFiles.git
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
GradientLoadingBar: 3c4246535efdedaf9b471c05caabfdb4b87b40a2
|
||||
SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b
|
||||
SwiftConfigurationFiles: 1cf2228a911ebed9f42f8dec077bb634f04ca6c8
|
||||
SwiftFormat: e73212c71908404e333da34e303772b9e516ac9b
|
||||
SwiftLint: 32ee33ded0636d0905ef6911b2b67bbaeeedafa5
|
||||
|
||||
PODFILE CHECKSUM: ea7965c23e5364bcd5cd7d3693d685af73a3e31d
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
-1711
File diff suppressed because it is too large
Load Diff
-68
@@ -1,68 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1240"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1A92A7C75080512A0168578180C192A2"
|
||||
BuildableName = "GradientLoadingBar-Unit-Tests.xctest"
|
||||
BlueprintName = "GradientLoadingBar-Unit-Tests"
|
||||
ReferencedContainer = "container:Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1A92A7C75080512A0168578180C192A2"
|
||||
BuildableName = "GradientLoadingBar-Unit-Tests.xctest"
|
||||
BlueprintName = "GradientLoadingBar-Unit-Tests"
|
||||
ReferencedContainer = "container:Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</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">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1240"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "AD9297F9E6CC3E50EDCFECA0A77800B7"
|
||||
BuildableName = "GradientLoadingBar.framework"
|
||||
BlueprintName = "GradientLoadingBar"
|
||||
ReferencedContainer = "container:Pods.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">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
Generated
-21
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Point-Free, Inc.
|
||||
|
||||
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.
|
||||
Generated
-244
@@ -1,244 +0,0 @@
|
||||
# 📸 SnapshotTesting
|
||||
|
||||
[](https://swift.org/download/)
|
||||
[](https://actions-badge.atrox.dev/pointfreeco/swift-snapshot-testing/goto)
|
||||
[](https://twitter.com/pointfreeco)
|
||||
|
||||
Delightful Swift snapshot testing.
|
||||
|
||||
<!--
|
||||

|
||||
-->
|
||||
|
||||
## Usage
|
||||
|
||||
Once [installed](#installation), _no additional configuration is required_. You can import the `SnapshotTesting` module and call the `assertSnapshot` function.
|
||||
|
||||
``` swift
|
||||
import SnapshotTesting
|
||||
import XCTest
|
||||
|
||||
class MyViewControllerTests: XCTestCase {
|
||||
func testMyViewController() {
|
||||
let vc = MyViewController()
|
||||
|
||||
assertSnapshot(matching: vc, as: .image)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When an assertion first runs, a snapshot is automatically recorded to disk and the test will fail, printing out the file path of any newly-recorded reference.
|
||||
|
||||
> 🛑 failed - No reference was found on disk. Automatically recorded snapshot: …
|
||||
>
|
||||
> open "…/MyAppTests/\_\_Snapshots\_\_/MyViewControllerTests/testMyViewController.png"
|
||||
>
|
||||
> Re-run "testMyViewController" to test against the newly-recorded snapshot.
|
||||
|
||||
Repeat test runs will load this reference and compare it with the runtime value. If they don't match, the test will fail and describe the difference. Failures can be inspected from Xcode's Report Navigator or by inspecting the file URLs of the failure.
|
||||
|
||||
You can record a new reference by setting the `record` parameter to `true` on the assertion or setting `isRecording` globally.
|
||||
|
||||
``` swift
|
||||
assertSnapshot(matching: vc, as: .image, record: true)
|
||||
|
||||
// or globally
|
||||
|
||||
isRecording = true
|
||||
assertSnapshot(matching: vc, as: .image)
|
||||
```
|
||||
|
||||
## Snapshot Anything
|
||||
|
||||
While most snapshot testing libraries in the Swift community are limited to `UIImage`s of `UIView`s, SnapshotTesting can work with _any_ format of _any_ value on _any_ Swift platform!
|
||||
|
||||
The `assertSnapshot` function accepts a value and any snapshot strategy that value supports. This means that a [view](Documentation/Available-Snapshot-Strategies.md#uiview) or [view controller](Documentation/Available-Snapshot-Strategies.md#uiviewcontroller) can be tested against an image representation _and_ against a textual representation of its properties and subview hierarchy.
|
||||
|
||||
``` swift
|
||||
assertSnapshot(matching: vc, as: .image)
|
||||
assertSnapshot(matching: vc, as: .recursiveDescription)
|
||||
```
|
||||
|
||||
View testing is [highly configurable](Documentation/Available-Snapshot-Strategies.md#uiviewcontroller). You can override trait collections (for specific size classes and content size categories) and generate device-agnostic snapshots, all from a single simulator.
|
||||
|
||||
``` swift
|
||||
assertSnapshot(matching: vc, as: .image(on: .iPhoneSe))
|
||||
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneSe))
|
||||
|
||||
assertSnapshot(matching: vc, as: .image(on: .iPhoneSe(.landscape)))
|
||||
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneSe(.landscape)))
|
||||
|
||||
assertSnapshot(matching: vc, as: .image(on: .iPhoneX))
|
||||
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneX))
|
||||
|
||||
assertSnapshot(matching: vc, as: .image(on: .iPadMini(.portrait)))
|
||||
assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPadMini(.portrait)))
|
||||
```
|
||||
|
||||
> ⚠️ Warning: Snapshots must be compared using a simulator with the same OS, device gamut, and scale as the simulator that originally took the reference to avoid discrepancies between images.
|
||||
|
||||
Better yet, SnapshotTesting isn't limited to views and view controllers! There are [a number of available snapshot strategies](Documentation/Available-Snapshot-Strategies.md) to choose from.
|
||||
|
||||
For example, you can snapshot test URL requests (_e.g._, those that your API client prepares).
|
||||
|
||||
``` swift
|
||||
assertSnapshot(matching: urlRequest, as: .raw)
|
||||
// POST http://localhost:8080/account
|
||||
// Cookie: pf_session={"userId":"1"}
|
||||
//
|
||||
// email=blob%40pointfree.co&name=Blob
|
||||
```
|
||||
|
||||
And you can snapshot test `Encodable` values against their JSON _and_ property list representations.
|
||||
|
||||
``` swift
|
||||
assertSnapshot(matching: user, as: .json)
|
||||
// {
|
||||
// "bio" : "Blobbed around the world.",
|
||||
// "id" : 1,
|
||||
// "name" : "Blobby"
|
||||
// }
|
||||
|
||||
assertSnapshot(matching: user, as: .plist)
|
||||
// <?xml version="1.0" encoding="UTF-8"?>
|
||||
// <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
// <plist version="1.0">
|
||||
// <dict>
|
||||
// <key>bio</key>
|
||||
// <string>Blobbed around the world.</string>
|
||||
// <key>id</key>
|
||||
// <integer>1</integer>
|
||||
// <key>name</key>
|
||||
// <string>Blobby</string>
|
||||
// </dict>
|
||||
// </plist>
|
||||
```
|
||||
|
||||
In fact, _[any](Documentation/Available-Snapshot-Strategies.md#any)_ value can be snapshot-tested by default using its [mirror](https://developer.apple.com/documentation/swift/mirror)!
|
||||
|
||||
``` swift
|
||||
assertSnapshot(matching: user, as: .dump)
|
||||
// ▿ User
|
||||
// - bio: "Blobbed around the world."
|
||||
// - id: 1
|
||||
// - name: "Blobby"
|
||||
```
|
||||
|
||||
If your data can be represented as an image, text, or data, you can write a snapshot test for it! Check out [all of the snapshot strategies](Documentation/Available-Snapshot-Strategies.md) that ship with SnapshotTesting and [learn how to define your own custom strategies](Documentation/Defining-Custom-Snapshot-Strategies.md).
|
||||
|
||||
## Installation
|
||||
|
||||
### Xcode 11
|
||||
|
||||
> ⚠️ Warning: By default, Xcode will try to add the SnapshotTesting package to your project's main application/framework target. Please ensure that SnapshotTesting is added to a _test_ target instead, as documented in the last step, below.
|
||||
|
||||
1. From the **File** menu, navigate through **Swift Packages** and select **Add Package Dependency…**.
|
||||
2. Enter package repository URL: `https://github.com/pointfreeco/swift-snapshot-testing.git`
|
||||
3. Confirm the version and let Xcode resolve the package
|
||||
4. On the final dialog, update SnapshotTesting's **Add to Target** column to a test target that will contain snapshot tests (if you have more than one test target, you can later add SnapshotTesting to them by manually linking the library in its build phase)
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
If you want to use SnapshotTesting in any other project that uses [SwiftPM](https://swift.org/package-manager/), add the package as a dependency in `Package.swift`:
|
||||
|
||||
```swift
|
||||
dependencies: [
|
||||
.package(name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.8.1"),
|
||||
]
|
||||
```
|
||||
|
||||
Next, add `SnapshotTesting` as a dependency of your test target:
|
||||
|
||||
```swift
|
||||
targets: [
|
||||
.target(name: "MyApp", dependencies: [], path: "Sources"),
|
||||
.testTarget(name: "MyAppTests", dependencies: ["MyApp", "SnapshotTesting"])
|
||||
]
|
||||
```
|
||||
|
||||
### Carthage
|
||||
|
||||
If you use [Carthage](https://github.com/Carthage/Carthage), you can add the following dependency to your `Cartfile`:
|
||||
|
||||
``` ruby
|
||||
github "pointfreeco/swift-snapshot-testing" ~> 1.8.0
|
||||
```
|
||||
|
||||
> ⚠️ Warning: Carthage instructs you to drag frameworks into your Xcode project. Xcode may automatically attempt to link these frameworks to your app target. `SnapshotTesting.framework` is only compatible with test targets, so when you first add it to your project:
|
||||
>
|
||||
> 1. Remove `SnapshotTesting.framework` from any non-test target it may have been added to.
|
||||
> 2. Add `SnapshotTesting.framework` to any applicable test targets.
|
||||
> 3. Add a **New Copy Build Phase** to any applicable test targets with **Destination** set to "Frameworks", and add `SnapshotTesting.framework` as an item to this phase.
|
||||
> 4. Do _not_ add `SnapshotTesting.framework` to the "Input Files" or "Output Files" of your app target's Carthage `copy-frameworks` **Run Script Phase**.
|
||||
>
|
||||
> See Carthage's "[Adding frameworks to unit tests or a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-unit-tests-or-a-framework)" documentation for more.
|
||||
|
||||
### CocoaPods
|
||||
|
||||
If your project uses [CocoaPods](https://cocoapods.org), add the pod to any applicable test targets in your `Podfile`:
|
||||
|
||||
```ruby
|
||||
target 'MyAppTests' do
|
||||
pod 'SnapshotTesting', '~> 1.8.1'
|
||||
end
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- [**Dozens of snapshot strategies**](Documentation/Available-Snapshot-Strategies.md). Snapshot testing isn't just for `UIView`s and `CALayer`s. Write snapshots against _any_ value.
|
||||
- [**Write your own snapshot strategies**](Documentation/Defining-Custom-Snapshot-Strategies.md). If you can convert it to an image, string, data, or your own diffable format, you can snapshot test it! Build your own snapshot strategies from scratch or transform existing ones.
|
||||
- **No configuration required.** Don't fuss with scheme settings and environment variables. Snapshots are automatically saved alongside your tests.
|
||||
- **More hands-off.** New snapshots are recorded whether `isRecording` mode is `true` or not.
|
||||
- **Subclass-free.** Assert from any XCTest case or Quick spec.
|
||||
- **Device-agnostic snapshots.** Render views and view controllers for specific devices and trait collections from a single simulator.
|
||||
- **First-class Xcode support.** Image differences are captured as XCTest attachments. Text differences are rendered in inline error messages.
|
||||
- **Supports any platform that supports Swift.** Write snapshot tests for iOS, Linux, macOS, and tvOS.
|
||||
- **SceneKit, SpriteKit, and WebKit support.** Most snapshot testing libraries don't support these view subclasses.
|
||||
- **`Codable` support**. Snapshot encodable data structures into their [JSON](Documentation/Available-Snapshot-Strategies.md#json) and [property list](Documentation/Available-Snapshot-Strategies.md#plist) representations.
|
||||
- **Custom diff tool integration**.
|
||||
|
||||
## Plug-ins
|
||||
|
||||
- [swift-snapshot-testing-nimble](https://github.com/Killectro/swift-snapshot-testing-nimble) adds [Nimble](https://github.com/Quick/Nimble) matchers for SnapshotTesting.
|
||||
|
||||
- [swift-html](https://github.com/pointfreeco/swift-html) is a Swift DSL for type-safe, extensible, and transformable HTML documents and includes an `HtmlSnapshotTesting` module to snapshot test its HTML documents.
|
||||
|
||||
- [GRDBSnapshotTesting](https://github.com/SebastianOsinski/GRDBSnapshotTesting) adds snapshot strategy for testing SQLite database migrations made with [GRDB](https://github.com/groue/GRDB.swift).
|
||||
|
||||
- [AccessibilitySnapshot](https://github.com/cashapp/AccessibilitySnapshot) adds easy regression testing for iOS accessibility.
|
||||
|
||||
- [AccessibilitySnapshotColorBlindness](https://github.com/Sherlouk/AccessibilitySnapshotColorBlindness) adds snapshot strategies for color blindness simulation on iOS views, view controllers and images.
|
||||
|
||||
Have you written your own SnapshotTesting plug-in? [Add it here](https://github.com/pointfreeco/swift-snapshot-testing/edit/master/README.md) and submit a pull request!
|
||||
|
||||
## Related Tools
|
||||
|
||||
- [`iOSSnapshotTestCase`](https://github.com/uber/ios-snapshot-test-case/) helped introduce screen shot testing to a broad audience in the iOS community. Experience with it inspired the creation of this library.
|
||||
|
||||
- [Jest](https://jestjs.io) brought generalized snapshot testing to the JavaScript community with a polished user experience. Several features of this library (diffing, automatically capturing new snapshots) were directly influenced.
|
||||
|
||||
## Learn More
|
||||
|
||||
SnapshotTesting was designed with [witness-oriented programming](https://www.pointfree.co/episodes/ep39-witness-oriented-library-design).
|
||||
|
||||
This concept (and more) are explored thoroughly in a series of episodes on [Point-Free](https://www.pointfree.co), a video series exploring functional programming and Swift hosted by [Brandon Williams](https://twitter.com/mbrandonw) and [Stephen Celis](https://twitter.com/stephencelis).
|
||||
|
||||
Witness-oriented programming and the design of this library was explored in the following [Point-Free](https://www.pointfree.co) episodes:
|
||||
|
||||
- [Episode 33](https://www.pointfree.co/episodes/ep33-protocol-witnesses-part-1): Protocol Witnesses: Part 1
|
||||
- [Episode 34](https://www.pointfree.co/episodes/ep34-protocol-witnesses-part-1): Protocol Witnesses: Part 2
|
||||
- [Episode 35](https://www.pointfree.co/episodes/ep35-advanced-protocol-witnesses-part-1): Advanced Protocol Witnesses: Part 1
|
||||
- [Episode 36](https://www.pointfree.co/episodes/ep36-advanced-protocol-witnesses-part-2): Advanced Protocol Witnesses: Part 2
|
||||
- [Episode 37](https://www.pointfree.co/episodes/ep37-protocol-oriented-library-design-part-1): Protocol-Oriented Library Design: Part 1
|
||||
- [Episode 38](https://www.pointfree.co/episodes/ep38-protocol-oriented-library-design-part-2): Protocol-Oriented Library Design: Part 2
|
||||
- [Episode 39](https://www.pointfree.co/episodes/ep39-witness-oriented-library-design): Witness-Oriented Library Design
|
||||
- [Episode 40](https://www.pointfree.co/episodes/ep40-async-functional-refactoring): Async Functional Refactoring
|
||||
- [Episode 41](https://www.pointfree.co/episodes/ep41-a-tour-of-snapshot-testing): A Tour of Snapshot Testing 🆓
|
||||
|
||||
<a href="https://www.pointfree.co/episodes/ep41-a-tour-of-snapshot-testing">
|
||||
<img alt="video poster image" src="https://d1hf1soyumxcgv.cloudfront.net/0041-tour-of-snapshot-testing/0041-poster.jpg" width="480">
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
This library is released under the MIT license. See [LICENSE](LICENSE) for details.
|
||||
-326
@@ -1,326 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
/// Asserts that a given value matches a string literal.
|
||||
///
|
||||
/// Note: Empty `reference` will be replaced automatically with generated output.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```
|
||||
/// _assertInlineSnapshot(matching: value, as: .dump, with: """
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: A value to compare against a reference.
|
||||
/// - snapshotting: A strategy for serializing, deserializing, and comparing values.
|
||||
/// - recording: Whether or not to record a new reference.
|
||||
/// - timeout: The amount of time a snapshot must be generated in.
|
||||
/// - reference: The expected output of snapshotting.
|
||||
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
|
||||
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
|
||||
public func _assertInlineSnapshot<Value>(
|
||||
matching value: @autoclosure () throws -> Value,
|
||||
as snapshotting: Snapshotting<Value, String>,
|
||||
record recording: Bool = false,
|
||||
timeout: TimeInterval = 5,
|
||||
with reference: String,
|
||||
file: StaticString = #file,
|
||||
testName: String = #function,
|
||||
line: UInt = #line
|
||||
) {
|
||||
|
||||
let failure = _verifyInlineSnapshot(
|
||||
matching: try value(),
|
||||
as: snapshotting,
|
||||
record: recording,
|
||||
timeout: timeout,
|
||||
with: reference,
|
||||
file: file,
|
||||
testName: testName,
|
||||
line: line
|
||||
)
|
||||
guard let message = failure else { return }
|
||||
XCTFail(message, file: file, line: line)
|
||||
}
|
||||
|
||||
/// Verifies that a given value matches a string literal.
|
||||
///
|
||||
/// Third party snapshot assert helpers can be built on top of this function. Simply invoke `verifyInlineSnapshot` with your own arguments, and then invoke `XCTFail` with the string returned if it is non-`nil`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: A value to compare against a reference.
|
||||
/// - snapshotting: A strategy for serializing, deserializing, and comparing values.
|
||||
/// - recording: Whether or not to record a new reference.
|
||||
/// - timeout: The amount of time a snapshot must be generated in.
|
||||
/// - reference: The expected output of snapshotting.
|
||||
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
|
||||
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
|
||||
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
|
||||
/// - Returns: A failure message or, if the value matches, nil.
|
||||
public func _verifyInlineSnapshot<Value>(
|
||||
matching value: @autoclosure () throws -> Value,
|
||||
as snapshotting: Snapshotting<Value, String>,
|
||||
record recording: Bool = false,
|
||||
timeout: TimeInterval = 5,
|
||||
with reference: String,
|
||||
file: StaticString = #file,
|
||||
testName: String = #function,
|
||||
line: UInt = #line
|
||||
)
|
||||
-> String? {
|
||||
|
||||
let recording = recording || isRecording
|
||||
|
||||
do {
|
||||
let tookSnapshot = XCTestExpectation(description: "Took snapshot")
|
||||
var optionalDiffable: String?
|
||||
snapshotting.snapshot(try value()).run { b in
|
||||
optionalDiffable = b
|
||||
tookSnapshot.fulfill()
|
||||
}
|
||||
let result = XCTWaiter.wait(for: [tookSnapshot], timeout: timeout)
|
||||
switch result {
|
||||
case .completed:
|
||||
break
|
||||
case .timedOut:
|
||||
return """
|
||||
Exceeded timeout of \(timeout) seconds waiting for snapshot.
|
||||
|
||||
This can happen when an asynchronously rendered view (like a web view) has not loaded. \
|
||||
Ensure that every subview of the view hierarchy has loaded to avoid timeouts, or, if a \
|
||||
timeout is unavoidable, consider setting the "timeout" parameter of "assertSnapshot" to \
|
||||
a higher value.
|
||||
"""
|
||||
case .incorrectOrder, .invertedFulfillment, .interrupted:
|
||||
return "Couldn't snapshot value"
|
||||
@unknown default:
|
||||
return "Couldn't snapshot value"
|
||||
}
|
||||
|
||||
let trimmingChars = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: "\u{FEFF}"))
|
||||
guard let diffable = optionalDiffable?.trimmingCharacters(in: trimmingChars) else {
|
||||
return "Couldn't snapshot value"
|
||||
}
|
||||
|
||||
let trimmedReference = reference.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Always perform diff, and return early on success!
|
||||
guard let (failure, attachments) = snapshotting.diffing.diff(trimmedReference, diffable) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If that diff failed, we either record or fail.
|
||||
if recording || trimmedReference.isEmpty {
|
||||
let fileName = "\(file)"
|
||||
let sourceCodeFilePath = URL(fileURLWithPath: fileName, isDirectory: false)
|
||||
let sourceCode = try String(contentsOf: sourceCodeFilePath)
|
||||
var newRecordings = recordings
|
||||
|
||||
let modifiedSource = try writeInlineSnapshot(
|
||||
&newRecordings,
|
||||
Context(
|
||||
sourceCode: sourceCode,
|
||||
diffable: diffable,
|
||||
fileName: fileName,
|
||||
lineIndex: Int(line)
|
||||
)
|
||||
).sourceCode
|
||||
|
||||
try modifiedSource
|
||||
.data(using: String.Encoding.utf8)?
|
||||
.write(to: sourceCodeFilePath)
|
||||
|
||||
if newRecordings != recordings {
|
||||
recordings = newRecordings
|
||||
/// If no other recording has been made, then fail!
|
||||
return """
|
||||
No reference was found inline. Automatically recorded snapshot.
|
||||
|
||||
Re-run "\(sanitizePathComponent(testName))" to test against the newly-recorded snapshot.
|
||||
"""
|
||||
} else {
|
||||
/// There is already an failure in this file,
|
||||
/// and we don't want to write to the wrong place.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Did not successfully record, so we will fail.
|
||||
if !attachments.isEmpty {
|
||||
#if !os(Linux)
|
||||
if ProcessInfo.processInfo.environment.keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
|
||||
XCTContext.runActivity(named: "Attached Failure Diff") { activity in
|
||||
attachments.forEach {
|
||||
activity.add($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return """
|
||||
Snapshot does not match reference.
|
||||
|
||||
\(failure.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
"""
|
||||
|
||||
} catch {
|
||||
return error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
internal typealias Recordings = [String: [FileRecording]]
|
||||
|
||||
internal struct Context {
|
||||
let sourceCode: String
|
||||
let diffable: String
|
||||
let fileName: String
|
||||
// First line of a file is line 1 (as with the #line macro)
|
||||
let lineIndex: Int
|
||||
|
||||
func setSourceCode(_ newSourceCode: String) -> Context {
|
||||
return Context(
|
||||
sourceCode: newSourceCode,
|
||||
diffable: diffable,
|
||||
fileName: fileName,
|
||||
lineIndex: lineIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal func writeInlineSnapshot(
|
||||
_ recordings: inout Recordings,
|
||||
_ context: Context
|
||||
) throws -> Context {
|
||||
var sourceCodeLines = context.sourceCode
|
||||
.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
|
||||
let otherRecordings = recordings[context.fileName, default: []]
|
||||
let otherRecordingsAboveThisLine = otherRecordings.filter { $0.line < context.lineIndex }
|
||||
let offsetStartIndex = otherRecordingsAboveThisLine.reduce(context.lineIndex) { $0 + $1.difference }
|
||||
let functionLineIndex = offsetStartIndex - 1
|
||||
var lineCountDifference = 0
|
||||
|
||||
// Convert `""` to multi-line literal
|
||||
if sourceCodeLines[functionLineIndex].hasSuffix(emptyStringLiteralWithCloseBrace) {
|
||||
// Convert:
|
||||
// _assertInlineSnapshot(matching: value, as: .dump, with: "")
|
||||
// to:
|
||||
// _assertInlineSnapshot(matching: value, as: .dump, with: """
|
||||
// """)
|
||||
var functionCallLine = sourceCodeLines.remove(at: functionLineIndex)
|
||||
functionCallLine.removeLast(emptyStringLiteralWithCloseBrace.count)
|
||||
let indentText = indentation(of: functionCallLine)
|
||||
sourceCodeLines.insert(contentsOf: [
|
||||
functionCallLine + multiLineStringLiteralTerminator,
|
||||
indentText + multiLineStringLiteralTerminator + ")",
|
||||
] as [String.SubSequence], at: functionLineIndex)
|
||||
lineCountDifference += 1
|
||||
}
|
||||
|
||||
/// If they haven't got a multi-line literal by now, then just fail.
|
||||
guard sourceCodeLines[functionLineIndex].hasSuffix(multiLineStringLiteralTerminator) else {
|
||||
struct InlineError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
return """
|
||||
To use inline snapshots, please convert the "with" argument to a multi-line literal.
|
||||
"""
|
||||
}
|
||||
}
|
||||
throw InlineError()
|
||||
}
|
||||
|
||||
/// Find the end of multi-line literal and replace contents with recording.
|
||||
if let multiLineLiteralEndIndex = sourceCodeLines[offsetStartIndex...].firstIndex(where: { $0.hasClosingMultilineStringDelimiter() }) {
|
||||
|
||||
let diffableLines = context.diffable.split(separator: "\n")
|
||||
|
||||
// Add #'s to the multiline string literal if needed
|
||||
let numberSigns: String
|
||||
if context.diffable.hasEscapedSpecialCharactersLiteral() {
|
||||
numberSigns = String(repeating: "#", count: context.diffable.numberOfNumberSignsNeeded())
|
||||
} else if nil != diffableLines.first(where: { $0.endsInBackslash() }) {
|
||||
// We want to avoid \ being interpreted as an escaped newline in the recorded inline snapshot
|
||||
numberSigns = "#"
|
||||
} else {
|
||||
numberSigns = ""
|
||||
}
|
||||
let multiLineStringLiteralTerminatorPre = numberSigns + multiLineStringLiteralTerminator
|
||||
let multiLineStringLiteralTerminatorPost = multiLineStringLiteralTerminator + numberSigns
|
||||
|
||||
// Update opening (#...)"""
|
||||
sourceCodeLines[functionLineIndex].replaceFirstOccurrence(
|
||||
of: extendedOpeningStringDelimitersPattern,
|
||||
with: multiLineStringLiteralTerminatorPre
|
||||
)
|
||||
|
||||
// Update closing """(#...)
|
||||
sourceCodeLines[multiLineLiteralEndIndex].replaceFirstOccurrence(
|
||||
of: extendedClosingStringDelimitersPattern,
|
||||
with: multiLineStringLiteralTerminatorPost
|
||||
)
|
||||
|
||||
/// Convert actual value to Lines to insert
|
||||
let indentText = indentation(of: sourceCodeLines[multiLineLiteralEndIndex])
|
||||
let newDiffableLines = context.diffable
|
||||
.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
.map { Substring(indentText + $0) }
|
||||
lineCountDifference += newDiffableLines.count - (multiLineLiteralEndIndex - offsetStartIndex)
|
||||
|
||||
let fileRecording = FileRecording(line: context.lineIndex, difference: lineCountDifference)
|
||||
|
||||
/// Insert the lines
|
||||
sourceCodeLines.replaceSubrange(offsetStartIndex..<multiLineLiteralEndIndex, with: newDiffableLines)
|
||||
|
||||
recordings[context.fileName, default: []].append(fileRecording)
|
||||
return context.setSourceCode(sourceCodeLines.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
return context.setSourceCode(sourceCodeLines.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
internal struct FileRecording: Equatable {
|
||||
let line: Int
|
||||
let difference: Int
|
||||
}
|
||||
|
||||
private func indentation<S: StringProtocol>(of str: S) -> String {
|
||||
var count = 0
|
||||
for char in str {
|
||||
guard char == " " else { break }
|
||||
count += 1
|
||||
}
|
||||
return String(repeating: " ", count: count)
|
||||
}
|
||||
|
||||
fileprivate extension Substring {
|
||||
mutating func replaceFirstOccurrence(of pattern: String, with newString: String) {
|
||||
let newString = replacingOccurrences(of: pattern, with: newString, options: .regularExpression)
|
||||
self = Substring(newString)
|
||||
}
|
||||
|
||||
func hasOpeningMultilineStringDelimiter() -> Bool {
|
||||
return range(of: extendedOpeningStringDelimitersPattern, options: .regularExpression) != nil
|
||||
}
|
||||
|
||||
func hasClosingMultilineStringDelimiter() -> Bool {
|
||||
return range(of: extendedClosingStringDelimitersPattern, options: .regularExpression) != nil
|
||||
}
|
||||
|
||||
func endsInBackslash() -> Bool {
|
||||
if let lastChar = last {
|
||||
return lastChar == Character(#"\"#)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private let emptyStringLiteralWithCloseBrace = "\"\")"
|
||||
private let multiLineStringLiteralTerminator = "\"\"\""
|
||||
private let extendedOpeningStringDelimitersPattern = #"#{0,}\"\"\""#
|
||||
private let extendedClosingStringDelimitersPattern = ##"\"\"\"#{0,}"##
|
||||
|
||||
// When we modify a file, the line numbers reported by the compiler through #line are no longer
|
||||
// accurate. With the FileRecording values we keep track of we modify the files so we can adjust
|
||||
// line numbers.
|
||||
private var recordings: Recordings = [:]
|
||||
@@ -1,313 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
/// Enhances failure messages with a command line diff tool expression that can be copied and pasted into a terminal.
|
||||
///
|
||||
/// diffTool = "ksdiff"
|
||||
public var diffTool: String? = nil
|
||||
|
||||
/// Whether or not to record all new references.
|
||||
public var isRecording = false
|
||||
|
||||
/// Whether or not to record all new references.
|
||||
/// Due to a name clash in Xcode 12, this has been renamed to `isRecording`.
|
||||
@available(*, deprecated, renamed: "isRecording")
|
||||
public var record: Bool {
|
||||
get { isRecording }
|
||||
set { isRecording = newValue }
|
||||
}
|
||||
|
||||
/// Asserts that a given value matches a reference on disk.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: A value to compare against a reference.
|
||||
/// - snapshotting: A strategy for serializing, deserializing, and comparing values.
|
||||
/// - name: An optional description of the snapshot.
|
||||
/// - recording: Whether or not to record a new reference.
|
||||
/// - timeout: The amount of time a snapshot must be generated in.
|
||||
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
|
||||
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
|
||||
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
|
||||
public func assertSnapshot<Value, Format>(
|
||||
matching value: @autoclosure () throws -> Value,
|
||||
as snapshotting: Snapshotting<Value, Format>,
|
||||
named name: String? = nil,
|
||||
record recording: Bool = false,
|
||||
timeout: TimeInterval = 5,
|
||||
file: StaticString = #file,
|
||||
testName: String = #function,
|
||||
line: UInt = #line
|
||||
) {
|
||||
|
||||
let failure = verifySnapshot(
|
||||
matching: try value(),
|
||||
as: snapshotting,
|
||||
named: name,
|
||||
record: recording,
|
||||
timeout: timeout,
|
||||
file: file,
|
||||
testName: testName,
|
||||
line: line
|
||||
)
|
||||
guard let message = failure else { return }
|
||||
XCTFail(message, file: file, line: line)
|
||||
}
|
||||
|
||||
/// Asserts that a given value matches references on disk.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: A value to compare against a reference.
|
||||
/// - snapshotting: A dictionary of names and strategies for serializing, deserializing, and comparing values.
|
||||
/// - recording: Whether or not to record a new reference.
|
||||
/// - timeout: The amount of time a snapshot must be generated in.
|
||||
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
|
||||
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
|
||||
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
|
||||
public func assertSnapshots<Value, Format>(
|
||||
matching value: @autoclosure () throws -> Value,
|
||||
as strategies: [String: Snapshotting<Value, Format>],
|
||||
record recording: Bool = false,
|
||||
timeout: TimeInterval = 5,
|
||||
file: StaticString = #file,
|
||||
testName: String = #function,
|
||||
line: UInt = #line
|
||||
) {
|
||||
|
||||
try? strategies.forEach { name, strategy in
|
||||
assertSnapshot(
|
||||
matching: try value(),
|
||||
as: strategy,
|
||||
named: name,
|
||||
record: recording,
|
||||
timeout: timeout,
|
||||
file: file,
|
||||
testName: testName,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that a given value matches references on disk.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: A value to compare against a reference.
|
||||
/// - snapshotting: An array of strategies for serializing, deserializing, and comparing values.
|
||||
/// - recording: Whether or not to record a new reference.
|
||||
/// - timeout: The amount of time a snapshot must be generated in.
|
||||
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
|
||||
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
|
||||
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
|
||||
public func assertSnapshots<Value, Format>(
|
||||
matching value: @autoclosure () throws -> Value,
|
||||
as strategies: [Snapshotting<Value, Format>],
|
||||
record recording: Bool = false,
|
||||
timeout: TimeInterval = 5,
|
||||
file: StaticString = #file,
|
||||
testName: String = #function,
|
||||
line: UInt = #line
|
||||
) {
|
||||
|
||||
try? strategies.forEach { strategy in
|
||||
assertSnapshot(
|
||||
matching: try value(),
|
||||
as: strategy,
|
||||
record: recording,
|
||||
timeout: timeout,
|
||||
file: file,
|
||||
testName: testName,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies that a given value matches a reference on disk.
|
||||
///
|
||||
/// Third party snapshot assert helpers can be built on top of this function. Simply invoke `verifySnapshot` with your own arguments, and then invoke `XCTFail` with the string returned if it is non-`nil`. For example, if you want the snapshot directory to be determined by an environment variable, you can create your own assert helper like so:
|
||||
///
|
||||
/// public func myAssertSnapshot<Value, Format>(
|
||||
/// matching value: @autoclosure () throws -> Value,
|
||||
/// as snapshotting: Snapshotting<Value, Format>,
|
||||
/// named name: String? = nil,
|
||||
/// record recording: Bool = false,
|
||||
/// timeout: TimeInterval = 5,
|
||||
/// file: StaticString = #file,
|
||||
/// testName: String = #function,
|
||||
/// line: UInt = #line
|
||||
/// ) {
|
||||
///
|
||||
/// let snapshotDirectory = ProcessInfo.processInfo.environment["SNAPSHOT_REFERENCE_DIR"]! + "/" + #file
|
||||
/// let failure = verifySnapshot(
|
||||
/// matching: value,
|
||||
/// as: snapshotting,
|
||||
/// named: name,
|
||||
/// record: recording,
|
||||
/// snapshotDirectory: snapshotDirectory,
|
||||
/// timeout: timeout,
|
||||
/// file: file,
|
||||
/// testName: testName
|
||||
/// )
|
||||
/// guard let message = failure else { return }
|
||||
/// XCTFail(message, file: file, line: line)
|
||||
/// }
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: A value to compare against a reference.
|
||||
/// - snapshotting: A strategy for serializing, deserializing, and comparing values.
|
||||
/// - name: An optional description of the snapshot.
|
||||
/// - recording: Whether or not to record a new reference.
|
||||
/// - snapshotDirectory: Optional directory to save snapshots. By default snapshots will be saved in a directory with the same name as the test file, and that directory will sit inside a directory `__Snapshots__` that sits next to your test file.
|
||||
/// - timeout: The amount of time a snapshot must be generated in.
|
||||
/// - file: The file in which failure occurred. Defaults to the file name of the test case in which this function was called.
|
||||
/// - testName: The name of the test in which failure occurred. Defaults to the function name of the test case in which this function was called.
|
||||
/// - line: The line number on which failure occurred. Defaults to the line number on which this function was called.
|
||||
/// - Returns: A failure message or, if the value matches, nil.
|
||||
public func verifySnapshot<Value, Format>(
|
||||
matching value: @autoclosure () throws -> Value,
|
||||
as snapshotting: Snapshotting<Value, Format>,
|
||||
named name: String? = nil,
|
||||
record recording: Bool = false,
|
||||
snapshotDirectory: String? = nil,
|
||||
timeout: TimeInterval = 5,
|
||||
file: StaticString = #file,
|
||||
testName: String = #function,
|
||||
line: UInt = #line
|
||||
)
|
||||
-> String? {
|
||||
|
||||
let recording = recording || isRecording
|
||||
|
||||
do {
|
||||
let fileUrl = URL(fileURLWithPath: "\(file)", isDirectory: false)
|
||||
let fileName = fileUrl.deletingPathExtension().lastPathComponent
|
||||
|
||||
let snapshotDirectoryUrl = snapshotDirectory.map { URL(fileURLWithPath: $0, isDirectory: true) }
|
||||
?? fileUrl
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("__Snapshots__")
|
||||
.appendingPathComponent(fileName)
|
||||
|
||||
let identifier: String
|
||||
if let name = name {
|
||||
identifier = sanitizePathComponent(name)
|
||||
} else {
|
||||
let counter = counterQueue.sync { () -> Int in
|
||||
let key = snapshotDirectoryUrl.appendingPathComponent(testName)
|
||||
counterMap[key, default: 0] += 1
|
||||
return counterMap[key]!
|
||||
}
|
||||
identifier = String(counter)
|
||||
}
|
||||
|
||||
let testName = sanitizePathComponent(testName)
|
||||
let snapshotFileUrl = snapshotDirectoryUrl
|
||||
.appendingPathComponent("\(testName).\(identifier)")
|
||||
.appendingPathExtension(snapshotting.pathExtension ?? "")
|
||||
let fileManager = FileManager.default
|
||||
try fileManager.createDirectory(at: snapshotDirectoryUrl, withIntermediateDirectories: true)
|
||||
|
||||
let tookSnapshot = XCTestExpectation(description: "Took snapshot")
|
||||
var optionalDiffable: Format?
|
||||
snapshotting.snapshot(try value()).run { b in
|
||||
optionalDiffable = b
|
||||
tookSnapshot.fulfill()
|
||||
}
|
||||
let result = XCTWaiter.wait(for: [tookSnapshot], timeout: timeout)
|
||||
switch result {
|
||||
case .completed:
|
||||
break
|
||||
case .timedOut:
|
||||
return """
|
||||
Exceeded timeout of \(timeout) seconds waiting for snapshot.
|
||||
|
||||
This can happen when an asynchronously rendered view (like a web view) has not loaded. \
|
||||
Ensure that every subview of the view hierarchy has loaded to avoid timeouts, or, if a \
|
||||
timeout is unavoidable, consider setting the "timeout" parameter of "assertSnapshot" to \
|
||||
a higher value.
|
||||
"""
|
||||
case .incorrectOrder, .invertedFulfillment, .interrupted:
|
||||
return "Couldn't snapshot value"
|
||||
@unknown default:
|
||||
return "Couldn't snapshot value"
|
||||
}
|
||||
|
||||
guard var diffable = optionalDiffable else {
|
||||
return "Couldn't snapshot value"
|
||||
}
|
||||
|
||||
guard !recording, fileManager.fileExists(atPath: snapshotFileUrl.path) else {
|
||||
try snapshotting.diffing.toData(diffable).write(to: snapshotFileUrl)
|
||||
return recording
|
||||
? """
|
||||
Record mode is on. Turn record mode off and re-run "\(testName)" to test against the newly-recorded snapshot.
|
||||
|
||||
open "\(snapshotFileUrl.path)"
|
||||
|
||||
Recorded snapshot: …
|
||||
"""
|
||||
: """
|
||||
No reference was found on disk. Automatically recorded snapshot: …
|
||||
|
||||
open "\(snapshotFileUrl.path)"
|
||||
|
||||
Re-run "\(testName)" to test against the newly-recorded snapshot.
|
||||
"""
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: snapshotFileUrl)
|
||||
let reference = snapshotting.diffing.fromData(data)
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
// If the image generation fails for the diffable part use the reference
|
||||
if let localDiff = diffable as? UIImage, localDiff.size == .zero {
|
||||
diffable = reference
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let (failure, attachments) = snapshotting.diffing.diff(reference, diffable) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let artifactsUrl = URL(
|
||||
fileURLWithPath: ProcessInfo.processInfo.environment["SNAPSHOT_ARTIFACTS"] ?? NSTemporaryDirectory(), isDirectory: true
|
||||
)
|
||||
let artifactsSubUrl = artifactsUrl.appendingPathComponent(fileName)
|
||||
try fileManager.createDirectory(at: artifactsSubUrl, withIntermediateDirectories: true)
|
||||
let failedSnapshotFileUrl = artifactsSubUrl.appendingPathComponent(snapshotFileUrl.lastPathComponent)
|
||||
try snapshotting.diffing.toData(diffable).write(to: failedSnapshotFileUrl)
|
||||
|
||||
if !attachments.isEmpty {
|
||||
#if !os(Linux)
|
||||
if ProcessInfo.processInfo.environment.keys.contains("__XCODE_BUILT_PRODUCTS_DIR_PATHS") {
|
||||
XCTContext.runActivity(named: "Attached Failure Diff") { activity in
|
||||
attachments.forEach {
|
||||
activity.add($0)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
let diffMessage = diffTool
|
||||
.map { "\($0) \"\(snapshotFileUrl.path)\" \"\(failedSnapshotFileUrl.path)\"" }
|
||||
?? "@\(minus)\n\"\(snapshotFileUrl.path)\"\n@\(plus)\n\"\(failedSnapshotFileUrl.path)\""
|
||||
return """
|
||||
Snapshot does not match reference.
|
||||
|
||||
\(diffMessage)
|
||||
|
||||
\(failure.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
"""
|
||||
} catch {
|
||||
return error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private let counterQueue = DispatchQueue(label: "co.pointfree.SnapshotTesting.counter")
|
||||
private var counterMap: [URL: Int] = [:]
|
||||
|
||||
func sanitizePathComponent(_ string: String) -> String {
|
||||
return string
|
||||
.replacingOccurrences(of: "\\W+", with: "-", options: .regularExpression)
|
||||
.replacingOccurrences(of: "^-|-$", with: "", options: .regularExpression)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/// A wrapper around an asynchronous operation.
|
||||
///
|
||||
/// Snapshot strategies may utilize this type to create snapshots in an asynchronous fashion.
|
||||
///
|
||||
/// For example, WebKit's `WKWebView` offers a callback-based API for taking image snapshots (`takeSnapshot`). `Async` allows us to build a value that can pass its callback along to the scope in which the image has been created.
|
||||
///
|
||||
/// Async<UIImage> { callback in
|
||||
/// webView.takeSnapshot(with: nil) { image, error in
|
||||
/// callback(image!)
|
||||
/// }
|
||||
/// }
|
||||
public struct Async<Value> {
|
||||
public let run: (@escaping (Value) -> Void) -> Void
|
||||
|
||||
/// Creates an asynchronous operation.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - run: A function that, when called, can hand a value to a callback.
|
||||
/// - callback: A function that can be called with a value.
|
||||
public init(run: @escaping (_ callback: @escaping (Value) -> Void) -> Void) {
|
||||
self.run = run
|
||||
}
|
||||
|
||||
/// Wraps a pure value in an asynchronous operation.
|
||||
///
|
||||
/// - Parameter value: A value to be wrapped in an asynchronous operation.
|
||||
public init(value: Value) {
|
||||
self.init { callback in callback(value) }
|
||||
}
|
||||
|
||||
/// Transforms an Async<Value> into an Async<NewValue> with a function `(Value) -> NewValue`.
|
||||
///
|
||||
/// - Parameter f: A transformation to apply to the value wrapped by the async value.
|
||||
public func map<NewValue>(_ f: @escaping (Value) -> NewValue) -> Async<NewValue> {
|
||||
return .init { callback in
|
||||
self.run { a in callback(f(a)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
typealias Image = NSImage
|
||||
typealias ImageView = NSImageView
|
||||
typealias View = NSView
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
typealias Image = UIImage
|
||||
typealias ImageView = UIImageView
|
||||
typealias View = UIView
|
||||
#endif
|
||||
-1828
File diff suppressed because it is too large
Load Diff
Generated
-57
@@ -1,57 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
|
||||
/// Checks whether the string has escaped special character literals or not.
|
||||
///
|
||||
/// This method won't detect an unescaped special character.
|
||||
/// For example, this method will return true for "\\n" or #"\n"#, but false for "\n"
|
||||
///
|
||||
/// The following are the special character literals that this methods looks for:
|
||||
/// The escaped special characters \0 (null character), \\ (backslash),
|
||||
/// \t (horizontal tab), \n (line feed), \r (carriage return),
|
||||
/// \" (double quotation mark) and \' (single quotation mark),
|
||||
/// An arbitrary Unicode scalar value, written as \u{n},
|
||||
/// where n is a 1–8 digit hexadecimal number (Unicode is discussed in Unicode below)
|
||||
/// The character sequence "#
|
||||
///
|
||||
/// - Returns: True if the string has any special character literals, false otherwise.
|
||||
func hasEscapedSpecialCharactersLiteral() -> Bool {
|
||||
let multilineLiteralAndNumberSign = ##"""
|
||||
"""#
|
||||
"""##
|
||||
let patterns = [
|
||||
// Matches \u{n} where n is a 1–8 digit hexadecimal number
|
||||
try? NSRegularExpression(pattern: #"\\u\{[a-fA-f0-9]{1,8}\}"#, options: .init()),
|
||||
try? NSRegularExpression(pattern: #"\0"#, options: .ignoreMetacharacters),
|
||||
try? NSRegularExpression(pattern: #"\\"#, options: .ignoreMetacharacters),
|
||||
try? NSRegularExpression(pattern: #"\t"#, options: .ignoreMetacharacters),
|
||||
try? NSRegularExpression(pattern: #"\n"#, options: .ignoreMetacharacters),
|
||||
try? NSRegularExpression(pattern: #"\r"#, options: .ignoreMetacharacters),
|
||||
try? NSRegularExpression(pattern: #"\""#, options: .ignoreMetacharacters),
|
||||
try? NSRegularExpression(pattern: #"\'"#, options: .ignoreMetacharacters),
|
||||
try? NSRegularExpression(pattern: multilineLiteralAndNumberSign, options: .ignoreMetacharacters),
|
||||
]
|
||||
let matches = patterns.compactMap { $0?.firstMatch(in: self, options: .init(), range: NSRange.init(location: 0, length: self.count)) }
|
||||
return matches.count > 0
|
||||
}
|
||||
|
||||
|
||||
/// This method calculates how many number signs (#) we need to add around a string
|
||||
/// literal to properly escape its content.
|
||||
///
|
||||
/// Multiple # are needed when the literal contains "#, "##, "### ...
|
||||
///
|
||||
/// - Returns: The number of "number signs(#)" needed around a string literal.
|
||||
/// When there is no "#, ... return 1
|
||||
func numberOfNumberSignsNeeded() -> Int {
|
||||
let pattern = try! NSRegularExpression(pattern: ##""#{1,}"##, options: .init())
|
||||
|
||||
let matches = pattern.matches(in: self, options: .init(), range: NSRange.init(location: 0, length: self.count))
|
||||
|
||||
// If we have "## then the length of the match is 3,
|
||||
// which is also the number of "number signs (#)" we need to add
|
||||
// before and after the string literal
|
||||
return matches.map { $0.range.length }.max() ?? 1
|
||||
}
|
||||
}
|
||||
@@ -1,891 +0,0 @@
|
||||
#if os(iOS) || os(macOS) || os(tvOS)
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#endif
|
||||
import SceneKit
|
||||
import SpriteKit
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
#endif
|
||||
#if os(iOS) || os(macOS)
|
||||
import WebKit
|
||||
#endif
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
public struct ViewImageConfig {
|
||||
public enum Orientation {
|
||||
case landscape
|
||||
case portrait
|
||||
}
|
||||
public enum TabletOrientation {
|
||||
public enum PortraitSplits {
|
||||
case oneThird
|
||||
case twoThirds
|
||||
case full
|
||||
}
|
||||
public enum LandscapeSplits {
|
||||
case oneThird
|
||||
case oneHalf
|
||||
case twoThirds
|
||||
case full
|
||||
}
|
||||
case landscape(splitView: LandscapeSplits)
|
||||
case portrait(splitView: PortraitSplits)
|
||||
}
|
||||
|
||||
public var safeArea: UIEdgeInsets
|
||||
public var size: CGSize?
|
||||
public var traits: UITraitCollection
|
||||
|
||||
public init(
|
||||
safeArea: UIEdgeInsets = .zero,
|
||||
size: CGSize? = nil,
|
||||
traits: UITraitCollection = .init()
|
||||
) {
|
||||
self.safeArea = safeArea
|
||||
self.size = size
|
||||
self.traits = traits
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
public static let iPhoneSe = ViewImageConfig.iPhoneSe(.portrait)
|
||||
|
||||
public static func iPhoneSe(_ orientation: Orientation) -> ViewImageConfig {
|
||||
let safeArea: UIEdgeInsets
|
||||
let size: CGSize
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
safeArea = .zero
|
||||
size = .init(width: 568, height: 320)
|
||||
case .portrait:
|
||||
safeArea = .init(top: 20, left: 0, bottom: 0, right: 0)
|
||||
size = .init(width: 320, height: 568)
|
||||
}
|
||||
return .init(safeArea: safeArea, size: size, traits: .iPhoneSe(orientation))
|
||||
}
|
||||
|
||||
public static let iPhone8 = ViewImageConfig.iPhone8(.portrait)
|
||||
|
||||
public static func iPhone8(_ orientation: Orientation) -> ViewImageConfig {
|
||||
let safeArea: UIEdgeInsets
|
||||
let size: CGSize
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
safeArea = .zero
|
||||
size = .init(width: 667, height: 375)
|
||||
case .portrait:
|
||||
safeArea = .init(top: 20, left: 0, bottom: 0, right: 0)
|
||||
size = .init(width: 375, height: 667)
|
||||
}
|
||||
return .init(safeArea: safeArea, size: size, traits: .iPhone8(orientation))
|
||||
}
|
||||
|
||||
public static let iPhone8Plus = ViewImageConfig.iPhone8Plus(.portrait)
|
||||
|
||||
public static func iPhone8Plus(_ orientation: Orientation) -> ViewImageConfig {
|
||||
let safeArea: UIEdgeInsets
|
||||
let size: CGSize
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
safeArea = .zero
|
||||
size = .init(width: 736, height: 414)
|
||||
case .portrait:
|
||||
safeArea = .init(top: 20, left: 0, bottom: 0, right: 0)
|
||||
size = .init(width: 414, height: 736)
|
||||
}
|
||||
return .init(safeArea: safeArea, size: size, traits: .iPhone8Plus(orientation))
|
||||
}
|
||||
|
||||
public static let iPhoneX = ViewImageConfig.iPhoneX(.portrait)
|
||||
|
||||
public static func iPhoneX(_ orientation: Orientation) -> ViewImageConfig {
|
||||
let safeArea: UIEdgeInsets
|
||||
let size: CGSize
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
safeArea = .init(top: 0, left: 44, bottom: 24, right: 44)
|
||||
size = .init(width: 812, height: 375)
|
||||
case .portrait:
|
||||
safeArea = .init(top: 44, left: 0, bottom: 34, right: 0)
|
||||
size = .init(width: 375, height: 812)
|
||||
}
|
||||
return .init(safeArea: safeArea, size: size, traits: .iPhoneX(orientation))
|
||||
}
|
||||
|
||||
public static let iPhoneXsMax = ViewImageConfig.iPhoneXsMax(.portrait)
|
||||
|
||||
public static func iPhoneXsMax(_ orientation: Orientation) -> ViewImageConfig {
|
||||
let safeArea: UIEdgeInsets
|
||||
let size: CGSize
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
safeArea = .init(top: 0, left: 44, bottom: 24, right: 44)
|
||||
size = .init(width: 896, height: 414)
|
||||
case .portrait:
|
||||
safeArea = .init(top: 44, left: 0, bottom: 34, right: 0)
|
||||
size = .init(width: 414, height: 896)
|
||||
}
|
||||
return .init(safeArea: safeArea, size: size, traits: .iPhoneXsMax(orientation))
|
||||
}
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
public static let iPhoneXr = ViewImageConfig.iPhoneXr(.portrait)
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
public static func iPhoneXr(_ orientation: Orientation) -> ViewImageConfig {
|
||||
let safeArea: UIEdgeInsets
|
||||
let size: CGSize
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
safeArea = .init(top: 0, left: 44, bottom: 24, right: 44)
|
||||
size = .init(width: 896, height: 414)
|
||||
case .portrait:
|
||||
safeArea = .init(top: 44, left: 0, bottom: 34, right: 0)
|
||||
size = .init(width: 414, height: 896)
|
||||
}
|
||||
return .init(safeArea: safeArea, size: size, traits: .iPhoneXr(orientation))
|
||||
}
|
||||
|
||||
public static let iPadMini = ViewImageConfig.iPadMini(.landscape)
|
||||
|
||||
public static func iPadMini(_ orientation: Orientation) -> ViewImageConfig {
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return ViewImageConfig.iPadMini(.landscape(splitView: .full))
|
||||
case .portrait:
|
||||
return ViewImageConfig.iPadMini(.portrait(splitView: .full))
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPadMini(_ orientation: TabletOrientation) -> ViewImageConfig {
|
||||
let size: CGSize
|
||||
let traits: UITraitCollection
|
||||
switch orientation {
|
||||
case .landscape(let splitView):
|
||||
switch splitView {
|
||||
case .oneThird:
|
||||
size = .init(width: 320, height: 768)
|
||||
traits = .iPadMini_Compact_SplitView
|
||||
case .oneHalf:
|
||||
size = .init(width: 507, height: 768)
|
||||
traits = .iPadMini_Compact_SplitView
|
||||
case .twoThirds:
|
||||
size = .init(width: 694, height: 768)
|
||||
traits = .iPadMini
|
||||
case .full:
|
||||
size = .init(width: 1024, height: 768)
|
||||
traits = .iPadMini
|
||||
}
|
||||
case .portrait(let splitView):
|
||||
switch splitView {
|
||||
case .oneThird:
|
||||
size = .init(width: 320, height: 1024)
|
||||
traits = .iPadMini_Compact_SplitView
|
||||
case .twoThirds:
|
||||
size = .init(width: 438, height: 1024)
|
||||
traits = .iPadMini_Compact_SplitView
|
||||
case .full:
|
||||
size = .init(width: 768, height: 1024)
|
||||
traits = .iPadMini
|
||||
}
|
||||
}
|
||||
return .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: size, traits: traits)
|
||||
}
|
||||
|
||||
public static let iPadPro10_5 = ViewImageConfig.iPadPro10_5(.landscape)
|
||||
|
||||
public static func iPadPro10_5(_ orientation: Orientation) -> ViewImageConfig {
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return ViewImageConfig.iPadPro10_5(.landscape(splitView: .full))
|
||||
case .portrait:
|
||||
return ViewImageConfig.iPadPro10_5(.portrait(splitView: .full))
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPadPro10_5(_ orientation: TabletOrientation) -> ViewImageConfig {
|
||||
let size: CGSize
|
||||
let traits: UITraitCollection
|
||||
switch orientation {
|
||||
case .landscape(let splitView):
|
||||
switch splitView {
|
||||
case .oneThird:
|
||||
size = .init(width: 320, height: 834)
|
||||
traits = .iPadPro10_5_Compact_SplitView
|
||||
case .oneHalf:
|
||||
size = .init(width: 551, height: 834)
|
||||
traits = .iPadPro10_5_Compact_SplitView
|
||||
case .twoThirds:
|
||||
size = .init(width: 782, height: 834)
|
||||
traits = .iPadPro10_5
|
||||
case .full:
|
||||
size = .init(width: 1112, height: 834)
|
||||
traits = .iPadPro10_5
|
||||
}
|
||||
case .portrait(let splitView):
|
||||
switch splitView {
|
||||
case .oneThird:
|
||||
size = .init(width: 320, height: 1112)
|
||||
traits = .iPadPro10_5_Compact_SplitView
|
||||
case .twoThirds:
|
||||
size = .init(width: 504, height: 1112)
|
||||
traits = .iPadPro10_5_Compact_SplitView
|
||||
case .full:
|
||||
size = .init(width: 834, height: 1112)
|
||||
traits = .iPadPro10_5
|
||||
}
|
||||
}
|
||||
return .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: size, traits: traits)
|
||||
}
|
||||
|
||||
public static let iPadPro11 = ViewImageConfig.iPadPro11(.landscape)
|
||||
|
||||
public static func iPadPro11(_ orientation: Orientation) -> ViewImageConfig {
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return ViewImageConfig.iPadPro11(.landscape(splitView: .full))
|
||||
case .portrait:
|
||||
return ViewImageConfig.iPadPro11(.portrait(splitView: .full))
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPadPro11(_ orientation: TabletOrientation) -> ViewImageConfig {
|
||||
let size: CGSize
|
||||
let traits: UITraitCollection
|
||||
switch orientation {
|
||||
case .landscape(let splitView):
|
||||
switch splitView {
|
||||
case .oneThird:
|
||||
size = .init(width: 375, height: 834)
|
||||
traits = .iPadPro11_Compact_SplitView
|
||||
case .oneHalf:
|
||||
size = .init(width: 592, height: 834)
|
||||
traits = .iPadPro11_Compact_SplitView
|
||||
case .twoThirds:
|
||||
size = .init(width: 809, height: 834)
|
||||
traits = .iPadPro11
|
||||
case .full:
|
||||
size = .init(width: 1194, height: 834)
|
||||
traits = .iPadPro11
|
||||
}
|
||||
case .portrait(let splitView):
|
||||
switch splitView {
|
||||
case .oneThird:
|
||||
size = .init(width: 320, height: 1194)
|
||||
traits = .iPadPro11_Compact_SplitView
|
||||
case .twoThirds:
|
||||
size = .init(width: 504, height: 1194)
|
||||
traits = .iPadPro11_Compact_SplitView
|
||||
case .full:
|
||||
size = .init(width: 834, height: 1194)
|
||||
traits = .iPadPro11
|
||||
}
|
||||
}
|
||||
return .init(safeArea: .init(top: 24, left: 0, bottom: 20, right: 0), size: size, traits: traits)
|
||||
}
|
||||
|
||||
public static let iPadPro12_9 = ViewImageConfig.iPadPro12_9(.landscape)
|
||||
|
||||
public static func iPadPro12_9(_ orientation: Orientation) -> ViewImageConfig {
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return ViewImageConfig.iPadPro12_9(.landscape(splitView: .full))
|
||||
case .portrait:
|
||||
return ViewImageConfig.iPadPro12_9(.portrait(splitView: .full))
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPadPro12_9(_ orientation: TabletOrientation) -> ViewImageConfig {
|
||||
let size: CGSize
|
||||
let traits: UITraitCollection
|
||||
switch orientation {
|
||||
case .landscape(let splitView):
|
||||
switch splitView {
|
||||
case .oneThird:
|
||||
size = .init(width: 375, height: 1024)
|
||||
traits = .iPadPro12_9_Compact_SplitView
|
||||
case .oneHalf:
|
||||
size = .init(width: 678, height: 1024)
|
||||
traits = .iPadPro12_9
|
||||
case .twoThirds:
|
||||
size = .init(width: 981, height: 1024)
|
||||
traits = .iPadPro12_9
|
||||
case .full:
|
||||
size = .init(width: 1366, height: 1024)
|
||||
traits = .iPadPro12_9
|
||||
}
|
||||
|
||||
case .portrait(let splitView):
|
||||
switch splitView {
|
||||
case .oneThird:
|
||||
size = .init(width: 375, height: 1366)
|
||||
traits = .iPadPro12_9_Compact_SplitView
|
||||
case .twoThirds:
|
||||
size = .init(width: 639, height: 1366)
|
||||
traits = .iPadPro12_9_Compact_SplitView
|
||||
case .full:
|
||||
size = .init(width: 1024, height: 1366)
|
||||
traits = .iPadPro12_9
|
||||
}
|
||||
|
||||
}
|
||||
return .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: size, traits: traits)
|
||||
}
|
||||
#elseif os(tvOS)
|
||||
public static let tv = ViewImageConfig(
|
||||
safeArea: .init(top: 60, left: 90, bottom: 60, right: 90),
|
||||
size: .init(width: 1920, height: 1080),
|
||||
traits: .init()
|
||||
)
|
||||
public static let tv4K = ViewImageConfig(
|
||||
safeArea: .init(top: 120, left: 180, bottom: 120, right: 180),
|
||||
size: .init(width: 3840, height: 2160),
|
||||
traits: .init()
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
extension UITraitCollection {
|
||||
#if os(iOS)
|
||||
public static func iPhoneSe(_ orientation: ViewImageConfig.Orientation)
|
||||
-> UITraitCollection {
|
||||
let base: [UITraitCollection] = [
|
||||
// .init(displayGamut: .SRGB),
|
||||
// .init(displayScale: 2),
|
||||
.init(forceTouchCapability: .available),
|
||||
.init(layoutDirection: .leftToRight),
|
||||
.init(preferredContentSizeCategory: .medium),
|
||||
.init(userInterfaceIdiom: .phone)
|
||||
]
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .compact)
|
||||
]
|
||||
)
|
||||
case .portrait:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .regular),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPhone8(_ orientation: ViewImageConfig.Orientation)
|
||||
-> UITraitCollection {
|
||||
let base: [UITraitCollection] = [
|
||||
// .init(displayGamut: .P3),
|
||||
// .init(displayScale: 2),
|
||||
.init(forceTouchCapability: .available),
|
||||
.init(layoutDirection: .leftToRight),
|
||||
.init(preferredContentSizeCategory: .medium),
|
||||
.init(userInterfaceIdiom: .phone)
|
||||
]
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .compact)
|
||||
]
|
||||
)
|
||||
case .portrait:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .regular)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPhone8Plus(_ orientation: ViewImageConfig.Orientation)
|
||||
-> UITraitCollection {
|
||||
let base: [UITraitCollection] = [
|
||||
// .init(displayGamut: .P3),
|
||||
// .init(displayScale: 3),
|
||||
.init(forceTouchCapability: .available),
|
||||
.init(layoutDirection: .leftToRight),
|
||||
.init(preferredContentSizeCategory: .medium),
|
||||
.init(userInterfaceIdiom: .phone)
|
||||
]
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .regular),
|
||||
.init(verticalSizeClass: .compact)
|
||||
]
|
||||
)
|
||||
case .portrait:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .regular)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPhoneX(_ orientation: ViewImageConfig.Orientation)
|
||||
-> UITraitCollection {
|
||||
let base: [UITraitCollection] = [
|
||||
// .init(displayGamut: .P3),
|
||||
// .init(displayScale: 3),
|
||||
.init(forceTouchCapability: .available),
|
||||
.init(layoutDirection: .leftToRight),
|
||||
.init(preferredContentSizeCategory: .medium),
|
||||
.init(userInterfaceIdiom: .phone)
|
||||
]
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .compact)
|
||||
]
|
||||
)
|
||||
case .portrait:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .regular)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPhoneXr(_ orientation: ViewImageConfig.Orientation)
|
||||
-> UITraitCollection {
|
||||
let base: [UITraitCollection] = [
|
||||
// .init(displayGamut: .P3),
|
||||
// .init(displayScale: 2),
|
||||
.init(forceTouchCapability: .unavailable),
|
||||
.init(layoutDirection: .leftToRight),
|
||||
.init(preferredContentSizeCategory: .medium),
|
||||
.init(userInterfaceIdiom: .phone)
|
||||
]
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .regular),
|
||||
.init(verticalSizeClass: .compact)
|
||||
]
|
||||
)
|
||||
case .portrait:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .regular)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static func iPhoneXsMax(_ orientation: ViewImageConfig.Orientation)
|
||||
-> UITraitCollection {
|
||||
let base: [UITraitCollection] = [
|
||||
// .init(displayGamut: .P3),
|
||||
// .init(displayScale: 3),
|
||||
.init(forceTouchCapability: .available),
|
||||
.init(layoutDirection: .leftToRight),
|
||||
.init(preferredContentSizeCategory: .medium),
|
||||
.init(userInterfaceIdiom: .phone)
|
||||
]
|
||||
switch orientation {
|
||||
case .landscape:
|
||||
return .init(
|
||||
traitsFrom: base + [
|
||||
.init(horizontalSizeClass: .regular),
|
||||
.init(verticalSizeClass: .compact)
|
||||
]
|
||||
)
|
||||
case .portrait:
|
||||
return .init(
|
||||
traitsFrom: [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .regular)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static let iPadMini = iPad
|
||||
public static let iPadMini_Compact_SplitView = iPadCompactSplitView
|
||||
public static let iPadPro10_5 = iPad
|
||||
public static let iPadPro10_5_Compact_SplitView = iPadCompactSplitView
|
||||
public static let iPadPro11 = iPad
|
||||
public static let iPadPro11_Compact_SplitView = iPadCompactSplitView
|
||||
public static let iPadPro12_9 = iPad
|
||||
public static let iPadPro12_9_Compact_SplitView = iPadCompactSplitView
|
||||
|
||||
private static let iPad = UITraitCollection(
|
||||
traitsFrom: [
|
||||
// .init(displayScale: 2),
|
||||
.init(horizontalSizeClass: .regular),
|
||||
.init(verticalSizeClass: .regular),
|
||||
.init(userInterfaceIdiom: .pad)
|
||||
]
|
||||
)
|
||||
|
||||
private static let iPadCompactSplitView = UITraitCollection(
|
||||
traitsFrom: [
|
||||
.init(horizontalSizeClass: .compact),
|
||||
.init(verticalSizeClass: .regular),
|
||||
.init(userInterfaceIdiom: .pad)
|
||||
]
|
||||
)
|
||||
#elseif os(tvOS)
|
||||
// TODO
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
func addImagesForRenderedViews(_ view: View) -> [Async<View>] {
|
||||
return view.snapshot
|
||||
.map { async in
|
||||
[
|
||||
Async { callback in
|
||||
async.run { image in
|
||||
let imageView = ImageView()
|
||||
imageView.image = image
|
||||
imageView.frame = view.frame
|
||||
#if os(macOS)
|
||||
view.superview?.addSubview(imageView, positioned: .above, relativeTo: view)
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
view.superview?.insertSubview(imageView, aboveSubview: view)
|
||||
#endif
|
||||
callback(imageView)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
?? view.subviews.flatMap(addImagesForRenderedViews)
|
||||
}
|
||||
|
||||
extension View {
|
||||
var snapshot: Async<Image>? {
|
||||
func inWindow<T>(_ perform: () -> T) -> T {
|
||||
#if os(macOS)
|
||||
let superview = self.superview
|
||||
defer { superview?.addSubview(self) }
|
||||
let window = ScaledWindow()
|
||||
window.contentView = NSView()
|
||||
window.contentView?.addSubview(self)
|
||||
window.makeKey()
|
||||
#endif
|
||||
return perform()
|
||||
}
|
||||
#if (os(iOS) && !targetEnvironment(macCatalyst)) || os(tvOS)
|
||||
if let glkView = self as? GLKView {
|
||||
return Async(value: inWindow { glkView.snapshot })
|
||||
}
|
||||
#endif
|
||||
if let scnView = self as? SCNView {
|
||||
return Async(value: inWindow { scnView.snapshot() })
|
||||
} else if let skView = self as? SKView {
|
||||
if #available(macOS 10.11, *) {
|
||||
let cgImage = inWindow { skView.texture(from: skView.scene!)!.cgImage() }
|
||||
#if os(macOS)
|
||||
let image = Image(cgImage: cgImage, size: skView.bounds.size)
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
let image = Image(cgImage: cgImage)
|
||||
#endif
|
||||
return Async(value: image)
|
||||
} else {
|
||||
fatalError("Taking SKView snapshots requires macOS 10.11 or greater")
|
||||
}
|
||||
}
|
||||
#if os(iOS) || os(macOS)
|
||||
if let wkWebView = self as? WKWebView {
|
||||
return Async<Image> { callback in
|
||||
let delegate = NavigationDelegate()
|
||||
let work = {
|
||||
if #available(iOS 11.0, macOS 10.13, *) {
|
||||
inWindow {
|
||||
guard wkWebView.frame.width != 0, wkWebView.frame.height != 0 else {
|
||||
callback(Image())
|
||||
return
|
||||
}
|
||||
wkWebView.takeSnapshot(with: nil) { image, _ in
|
||||
_ = delegate
|
||||
callback(image!)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#if os(iOS)
|
||||
fatalError("Taking WKWebView snapshots requires iOS 11.0 or greater")
|
||||
#elseif os(macOS)
|
||||
fatalError("Taking WKWebView snapshots requires macOS 10.13 or greater")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if wkWebView.isLoading {
|
||||
delegate.didFinish = work
|
||||
wkWebView.navigationDelegate = delegate
|
||||
} else {
|
||||
work()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
#if os(iOS) || os(tvOS)
|
||||
func asImage() -> Image {
|
||||
let renderer = UIGraphicsImageRenderer(bounds: bounds)
|
||||
return renderer.image { rendererContext in
|
||||
layer.render(in: rendererContext.cgContext)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS) || os(macOS)
|
||||
private final class NavigationDelegate: NSObject, WKNavigationDelegate {
|
||||
var didFinish: () -> Void
|
||||
|
||||
init(didFinish: @escaping () -> Void = {}) {
|
||||
self.didFinish = didFinish
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
webView.evaluateJavaScript("document.readyState") { _, _ in
|
||||
self.didFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
extension UIApplication {
|
||||
static var sharedIfAvailable: UIApplication? {
|
||||
let sharedSelector = NSSelectorFromString("sharedApplication")
|
||||
guard UIApplication.responds(to: sharedSelector) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let shared = UIApplication.perform(sharedSelector)
|
||||
return shared?.takeUnretainedValue() as! UIApplication?
|
||||
}
|
||||
}
|
||||
|
||||
func prepareView(
|
||||
config: ViewImageConfig,
|
||||
drawHierarchyInKeyWindow: Bool,
|
||||
traits: UITraitCollection,
|
||||
view: UIView,
|
||||
viewController: UIViewController
|
||||
) -> () -> Void {
|
||||
let size = config.size ?? viewController.view.frame.size
|
||||
view.frame.size = size
|
||||
if view != viewController.view {
|
||||
viewController.view.bounds = view.bounds
|
||||
viewController.view.addSubview(view)
|
||||
}
|
||||
let traits = UITraitCollection(traitsFrom: [config.traits, traits])
|
||||
let window: UIWindow
|
||||
if drawHierarchyInKeyWindow {
|
||||
guard let keyWindow = getKeyWindow() else {
|
||||
fatalError("'drawHierarchyInKeyWindow' requires tests to be run in a host application")
|
||||
}
|
||||
window = keyWindow
|
||||
window.frame.size = size
|
||||
} else {
|
||||
window = Window(
|
||||
config: .init(safeArea: config.safeArea, size: config.size ?? size, traits: traits),
|
||||
viewController: viewController
|
||||
)
|
||||
}
|
||||
let dispose = add(traits: traits, viewController: viewController, to: window)
|
||||
|
||||
if size.width == 0 || size.height == 0 {
|
||||
// Try to call sizeToFit() if the view still has invalid size
|
||||
view.sizeToFit()
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
return dispose
|
||||
}
|
||||
|
||||
func snapshotView(
|
||||
config: ViewImageConfig,
|
||||
drawHierarchyInKeyWindow: Bool,
|
||||
traits: UITraitCollection,
|
||||
view: UIView,
|
||||
viewController: UIViewController
|
||||
)
|
||||
-> Async<UIImage> {
|
||||
let initialFrame = view.frame
|
||||
let dispose = prepareView(
|
||||
config: config,
|
||||
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
|
||||
traits: traits,
|
||||
view: view,
|
||||
viewController: viewController
|
||||
)
|
||||
// NB: Avoid safe area influence.
|
||||
if config.safeArea == .zero { view.frame.origin = .init(x: offscreen, y: offscreen) }
|
||||
|
||||
return (view.snapshot ?? Async { callback in
|
||||
addImagesForRenderedViews(view).sequence().run { views in
|
||||
callback(
|
||||
renderer(bounds: view.bounds, for: traits).image { ctx in
|
||||
if drawHierarchyInKeyWindow {
|
||||
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
|
||||
} else {
|
||||
view.layer.render(in: ctx.cgContext)
|
||||
}
|
||||
}
|
||||
)
|
||||
views.forEach { $0.removeFromSuperview() }
|
||||
view.frame = initialFrame
|
||||
}
|
||||
}).map { dispose(); return $0 }
|
||||
}
|
||||
|
||||
private let offscreen: CGFloat = 10_000
|
||||
|
||||
func renderer(bounds: CGRect, for traits: UITraitCollection) -> UIGraphicsImageRenderer {
|
||||
let renderer: UIGraphicsImageRenderer
|
||||
if #available(iOS 11.0, tvOS 11.0, *) {
|
||||
renderer = UIGraphicsImageRenderer(bounds: bounds, format: .init(for: traits))
|
||||
} else {
|
||||
renderer = UIGraphicsImageRenderer(bounds: bounds)
|
||||
}
|
||||
return renderer
|
||||
}
|
||||
|
||||
private func add(traits: UITraitCollection, viewController: UIViewController, to window: UIWindow) -> () -> Void {
|
||||
let rootViewController: UIViewController
|
||||
if viewController != window.rootViewController {
|
||||
rootViewController = UIViewController()
|
||||
rootViewController.view.backgroundColor = .clear
|
||||
rootViewController.view.frame = window.frame
|
||||
rootViewController.view.translatesAutoresizingMaskIntoConstraints =
|
||||
viewController.view.translatesAutoresizingMaskIntoConstraints
|
||||
rootViewController.preferredContentSize = rootViewController.view.frame.size
|
||||
viewController.view.frame = rootViewController.view.frame
|
||||
rootViewController.view.addSubview(viewController.view)
|
||||
if viewController.view.translatesAutoresizingMaskIntoConstraints {
|
||||
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
} else {
|
||||
NSLayoutConstraint.activate([
|
||||
viewController.view.topAnchor.constraint(equalTo: rootViewController.view.topAnchor),
|
||||
viewController.view.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor),
|
||||
viewController.view.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor),
|
||||
viewController.view.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor),
|
||||
])
|
||||
}
|
||||
rootViewController.addChild(viewController)
|
||||
} else {
|
||||
rootViewController = viewController
|
||||
}
|
||||
rootViewController.setOverrideTraitCollection(traits, forChild: viewController)
|
||||
viewController.didMove(toParent: rootViewController)
|
||||
|
||||
window.rootViewController = rootViewController
|
||||
|
||||
rootViewController.beginAppearanceTransition(true, animated: false)
|
||||
rootViewController.endAppearanceTransition()
|
||||
|
||||
rootViewController.view.setNeedsLayout()
|
||||
rootViewController.view.layoutIfNeeded()
|
||||
|
||||
viewController.view.setNeedsLayout()
|
||||
viewController.view.layoutIfNeeded()
|
||||
|
||||
return {
|
||||
rootViewController.beginAppearanceTransition(false, animated: false)
|
||||
rootViewController.endAppearanceTransition()
|
||||
window.rootViewController = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func getKeyWindow() -> UIWindow? {
|
||||
var window: UIWindow?
|
||||
if #available(iOS 13.0, *) {
|
||||
window = UIApplication.sharedIfAvailable?.windows.first { $0.isKeyWindow }
|
||||
} else {
|
||||
window = UIApplication.sharedIfAvailable?.keyWindow
|
||||
}
|
||||
return window
|
||||
}
|
||||
|
||||
private final class Window: UIWindow {
|
||||
var config: ViewImageConfig
|
||||
|
||||
init(config: ViewImageConfig, viewController: UIViewController) {
|
||||
let size = config.size ?? viewController.view.bounds.size
|
||||
self.config = config
|
||||
super.init(frame: .init(origin: .zero, size: size))
|
||||
|
||||
// NB: Safe area renders inaccurately for UI{Navigation,TabBar}Controller.
|
||||
// Fixes welcome!
|
||||
if viewController is UINavigationController {
|
||||
self.frame.size.height -= self.config.safeArea.top
|
||||
self.config.safeArea.top = 0
|
||||
} else if let viewController = viewController as? UITabBarController {
|
||||
self.frame.size.height -= self.config.safeArea.bottom
|
||||
self.config.safeArea.bottom = 0
|
||||
if viewController.selectedViewController is UINavigationController {
|
||||
self.frame.size.height -= self.config.safeArea.top
|
||||
self.config.safeArea.top = 0
|
||||
}
|
||||
}
|
||||
self.isHidden = false
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@available(iOS 11.0, *)
|
||||
override var safeAreaInsets: UIEdgeInsets {
|
||||
#if os(iOS)
|
||||
let removeTopInset = self.config.safeArea == .init(top: 20, left: 0, bottom: 0, right: 0)
|
||||
&& self.rootViewController?.prefersStatusBarHidden ?? false
|
||||
if removeTopInset { return .zero }
|
||||
#endif
|
||||
return self.config.safeArea
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
|
||||
private final class ScaledWindow: NSWindow {
|
||||
override var backingScaleFactor: CGFloat {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extension Array {
|
||||
func sequence<A>() -> Async<[A]> where Element == Async<A> {
|
||||
guard !self.isEmpty else { return Async(value: []) }
|
||||
return Async<[A]> { callback in
|
||||
var result = [A?](repeating: nil, count: self.count)
|
||||
result.reserveCapacity(self.count)
|
||||
var count = 0
|
||||
zip(self.indices, self).forEach { idx, async in
|
||||
async.run {
|
||||
result[idx] = $0
|
||||
count += 1
|
||||
if count == self.count {
|
||||
callback(result as! [A])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
#if os(Linux)
|
||||
import Foundation
|
||||
|
||||
public struct XCTAttachment {
|
||||
public init(data: Data) {}
|
||||
public init(data: Data, uniformTypeIdentifier: String) {}
|
||||
}
|
||||
#endif
|
||||
@@ -1,125 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct Difference<A> {
|
||||
enum Which {
|
||||
case first
|
||||
case second
|
||||
case both
|
||||
}
|
||||
|
||||
let elements: [A]
|
||||
let which: Which
|
||||
}
|
||||
|
||||
func diff<A: Hashable>(_ fst: [A], _ snd: [A]) -> [Difference<A>] {
|
||||
var idxsOf = [A: [Int]]()
|
||||
fst.enumerated().forEach { idxsOf[$1, default: []].append($0) }
|
||||
|
||||
let sub = snd.enumerated().reduce((overlap: [Int: Int](), fst: 0, snd: 0, len: 0)) { sub, sndPair in
|
||||
(idxsOf[sndPair.element] ?? [])
|
||||
.reduce((overlap: [Int: Int](), fst: sub.fst, snd: sub.snd, len: sub.len)) { innerSub, fstIdx in
|
||||
|
||||
var newOverlap = innerSub.overlap
|
||||
newOverlap[fstIdx] = (sub.overlap[fstIdx - 1] ?? 0) + 1
|
||||
|
||||
if let newLen = newOverlap[fstIdx], newLen > sub.len {
|
||||
return (newOverlap, fstIdx - newLen + 1, sndPair.offset - newLen + 1, newLen)
|
||||
}
|
||||
return (newOverlap, innerSub.fst, innerSub.snd, innerSub.len)
|
||||
}
|
||||
}
|
||||
let (_, fstIdx, sndIdx, len) = sub
|
||||
|
||||
if len == 0 {
|
||||
let fstDiff = fst.isEmpty ? [] : [Difference(elements: fst, which: .first)]
|
||||
let sndDiff = snd.isEmpty ? [] : [Difference(elements: snd, which: .second)]
|
||||
return fstDiff + sndDiff
|
||||
} else {
|
||||
let fstDiff = diff(Array(fst.prefix(upTo: fstIdx)), Array(snd.prefix(upTo: sndIdx)))
|
||||
let midDiff = [Difference(elements: Array(fst.suffix(from: fstIdx).prefix(len)), which: .both)]
|
||||
let lstDiff = diff(Array(fst.suffix(from: fstIdx + len)), Array(snd.suffix(from: sndIdx + len)))
|
||||
return fstDiff + midDiff + lstDiff
|
||||
}
|
||||
}
|
||||
|
||||
let minus = "−"
|
||||
let plus = "+"
|
||||
private let figureSpace = "\u{2007}"
|
||||
|
||||
struct Hunk {
|
||||
let fstIdx: Int
|
||||
let fstLen: Int
|
||||
let sndIdx: Int
|
||||
let sndLen: Int
|
||||
let lines: [String]
|
||||
|
||||
var patchMark: String {
|
||||
let fstMark = "\(minus)\(fstIdx + 1),\(fstLen)"
|
||||
let sndMark = "\(plus)\(sndIdx + 1),\(sndLen)"
|
||||
return "@@ \(fstMark) \(sndMark) @@"
|
||||
}
|
||||
|
||||
// Semigroup
|
||||
|
||||
static func +(lhs: Hunk, rhs: Hunk) -> Hunk {
|
||||
return Hunk(
|
||||
fstIdx: lhs.fstIdx + rhs.fstIdx,
|
||||
fstLen: lhs.fstLen + rhs.fstLen,
|
||||
sndIdx: lhs.sndIdx + rhs.sndIdx,
|
||||
sndLen: lhs.sndLen + rhs.sndLen,
|
||||
lines: lhs.lines + rhs.lines
|
||||
)
|
||||
}
|
||||
|
||||
// Monoid
|
||||
|
||||
init(fstIdx: Int = 0, fstLen: Int = 0, sndIdx: Int = 0, sndLen: Int = 0, lines: [String] = []) {
|
||||
self.fstIdx = fstIdx
|
||||
self.fstLen = fstLen
|
||||
self.sndIdx = sndIdx
|
||||
self.sndLen = sndLen
|
||||
self.lines = lines
|
||||
}
|
||||
|
||||
init(idx: Int = 0, len: Int = 0, lines: [String] = []) {
|
||||
self.init(fstIdx: idx, fstLen: len, sndIdx: idx, sndLen: len, lines: lines)
|
||||
}
|
||||
}
|
||||
|
||||
func chunk(diff diffs: [Difference<String>], context ctx: Int = 4) -> [Hunk] {
|
||||
func prepending(_ prefix: String) -> (String) -> String {
|
||||
return { prefix + $0 + ($0.hasSuffix(" ") ? "¬" : "") }
|
||||
}
|
||||
let changed: (Hunk) -> Bool = { $0.lines.contains(where: { $0.hasPrefix(minus) || $0.hasPrefix(plus) }) }
|
||||
|
||||
let (hunk, hunks) = diffs
|
||||
.reduce((current: Hunk(), hunks: [Hunk]())) { cursor, diff in
|
||||
let (current, hunks) = cursor
|
||||
let len = diff.elements.count
|
||||
|
||||
switch diff.which {
|
||||
case .both where len > ctx * 2:
|
||||
let hunk = current + Hunk(len: ctx, lines: diff.elements.prefix(ctx).map(prepending(figureSpace)))
|
||||
let next = Hunk(
|
||||
fstIdx: current.fstIdx + current.fstLen + len - ctx,
|
||||
fstLen: ctx,
|
||||
sndIdx: current.sndIdx + current.sndLen + len - ctx,
|
||||
sndLen: ctx,
|
||||
lines: (diff.elements.suffix(ctx) as ArraySlice<String>).map(prepending(figureSpace))
|
||||
)
|
||||
return (next, changed(hunk) ? hunks + [hunk] : hunks)
|
||||
case .both where current.lines.isEmpty:
|
||||
let lines = (diff.elements.suffix(ctx) as ArraySlice<String>).map(prepending(figureSpace))
|
||||
let count = lines.count
|
||||
return (current + Hunk(idx: len - count, len: count, lines: lines), hunks)
|
||||
case .both:
|
||||
return (current + Hunk(len: len, lines: diff.elements.map(prepending(figureSpace))), hunks)
|
||||
case .first:
|
||||
return (current + Hunk(fstLen: len, lines: diff.elements.map(prepending(minus))), hunks)
|
||||
case .second:
|
||||
return (current + Hunk(sndLen: len, lines: diff.elements.map(prepending(plus))), hunks)
|
||||
}
|
||||
}
|
||||
|
||||
return changed(hunk) ? hunks + [hunk] : hunks
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
/// The ability to compare `Value`s and convert them to and from `Data`.
|
||||
public struct Diffing<Value> {
|
||||
/// Converts a value _to_ data.
|
||||
public var toData: (Value) -> Data
|
||||
|
||||
/// Produces a value _from_ data.
|
||||
public var fromData: (Data) -> Value
|
||||
|
||||
/// Compares two values. If the values do not match, returns a failure message and artifacts describing the failure.
|
||||
public var diff: (Value, Value) -> (String, [XCTAttachment])?
|
||||
|
||||
/// Creates a new `Diffing` on `Value`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - toData: A function used to convert a value _to_ data.
|
||||
/// - value: A value to convert into data.
|
||||
/// - fromData: A function used to produce a value _from_ data.
|
||||
/// - data: Data to convert into a value.
|
||||
/// - diff: A function used to compare two values. If the values do not match, returns a failure message and artifacts describing the failure.
|
||||
/// - lhs: A value to compare.
|
||||
/// - rhs: Another value to compare.
|
||||
public init(
|
||||
toData: @escaping (_ value: Value) -> Data,
|
||||
fromData: @escaping (_ data: Data) -> Value,
|
||||
diff: @escaping (_ lhs: Value, _ rhs: Value) -> (String, [XCTAttachment])?
|
||||
) {
|
||||
self.toData = toData
|
||||
self.fromData = fromData
|
||||
self.diff = diff
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
extension Snapshotting {
|
||||
/// Transforms an existing snapshot strategy into one that waits for some amount of time before taking the snapshot. This can be useful for waiting for animations to complete or for UIKit events to finish (i.e. waiting for a UINavigationController to push a child onto the stack).
|
||||
/// - Parameters:
|
||||
/// - duration: The amount of time to wait before taking the snapshot.
|
||||
/// - strategy: The snapshot to invoke after the specified amount of time has passed.
|
||||
public static func wait(
|
||||
for duration: TimeInterval,
|
||||
on strategy: Snapshotting
|
||||
) -> Snapshotting {
|
||||
return Snapshotting(
|
||||
pathExtension: strategy.pathExtension,
|
||||
diffing: strategy.diffing,
|
||||
asyncSnapshot: { value in
|
||||
Async { callback in
|
||||
let expectation = XCTestExpectation(description: "Wait")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
|
||||
expectation.fulfill()
|
||||
}
|
||||
_ = XCTWaiter.wait(for: [expectation], timeout: duration + 1)
|
||||
strategy.snapshot(value).run(callback)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
@available(swift, obsoleted: 5.0, renamed: "XCTestCase", message: "Please use XCTestCase instead")
|
||||
public typealias SnapshotTestCase = XCTestCase
|
||||
@@ -1,106 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
/// A type representing the ability to transform a snapshottable value into a diffable format (like text or an image) for snapshot testing.
|
||||
public struct Snapshotting<Value, Format> {
|
||||
/// The path extension applied to references saved to disk.
|
||||
public var pathExtension: String?
|
||||
|
||||
/// How the snapshot format is diffed and converted to and from data.
|
||||
public var diffing: Diffing<Format>
|
||||
|
||||
/// How a value is transformed into a diffable snapshot format.
|
||||
public var snapshot: (Value) -> Async<Format>
|
||||
|
||||
/// Creates a snapshot strategy.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - pathExtension: The path extension applied to references saved to disk.
|
||||
/// - diffing: How to diff and convert the snapshot format to and from data.
|
||||
/// - snapshot: An asynchronous transform function from a value into a diffable snapshot format.
|
||||
/// - value: A value to be converted.
|
||||
public init(
|
||||
pathExtension: String?,
|
||||
diffing: Diffing<Format>,
|
||||
asyncSnapshot: @escaping (_ value: Value) -> Async<Format>
|
||||
) {
|
||||
self.pathExtension = pathExtension
|
||||
self.diffing = diffing
|
||||
self.snapshot = asyncSnapshot
|
||||
}
|
||||
|
||||
/// Creates a snapshot strategy.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - pathExtension: The path extension applied to references saved to disk.
|
||||
/// - diffing: How to diff and convert the snapshot format to and from data.
|
||||
/// - snapshot: A transform function from a value into a diffable snapshot format.
|
||||
/// - value: A snapshot value to be converted.
|
||||
public init(
|
||||
pathExtension: String?,
|
||||
diffing: Diffing<Format>,
|
||||
snapshot: @escaping (_ value: Value) -> Format
|
||||
) {
|
||||
self.init(pathExtension: pathExtension, diffing: diffing) {
|
||||
Async(value: snapshot($0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms a strategy on `Value`s into a strategy on `NewValue`s through a function `(NewValue) -> Value`.
|
||||
///
|
||||
/// This is the most important operation for transforming existing strategies into new strategies. It allows you to transform a `Snapshotting<Value, Format>` into a `Snapshotting<NewValue, Format>` by pulling it back along a function `(NewValue) -> Value`. Notice that the function must go in the direction `(NewValue) -> Value` even though we are transforming in the other direction `(Snapshotting<Value, Format>) -> Snapshotting<NewValue, Format>`.
|
||||
///
|
||||
/// A simple example of this is to `pullback` the snapshot strategy on `UIView`s to work on `UIViewController`s:
|
||||
///
|
||||
/// let strategy = Snapshotting<UIView, UIImage>.image.pullback { (vc: UIViewController) in
|
||||
/// return vc.view
|
||||
/// }
|
||||
///
|
||||
/// Here we took the strategy that snapshots `UIView`s as `UIImage`s and pulled it back to work on `UIViewController`s by using the function `(UIViewController) -> UIView` that simply plucks the view out of the controller.
|
||||
///
|
||||
/// Nearly every snapshot strategy provided in this library is a pullback of some base strategy, which shows just how important this operation is.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - transform: A transform function from `NewValue` into `Value`.
|
||||
/// - otherValue: A value to be transformed.
|
||||
public func pullback<NewValue>(_ transform: @escaping (_ otherValue: NewValue) -> Value) -> Snapshotting<NewValue, Format> {
|
||||
return self.asyncPullback { newValue in Async(value: transform(newValue)) }
|
||||
}
|
||||
|
||||
/// Transforms a strategy on `Value`s into a strategy on `NewValue`s through a function `(NewValue) -> Async<Value>`.
|
||||
///
|
||||
/// See the documention of `pullback` for a full description of how pullbacks works. This operation differs from `pullback` in that it allows you to use a transformation `(NewValue) -> Async<Value>`, which is necessary when your transformation needs to perform some asynchronous work.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - transform: A transform function from `NewValue` into `Async<Value>`.
|
||||
/// - otherValue: A value to be transformed.
|
||||
public func asyncPullback<NewValue>(_ transform: @escaping (_ otherValue: NewValue) -> Async<Value>)
|
||||
-> Snapshotting<NewValue, Format> {
|
||||
|
||||
return Snapshotting<NewValue, Format>(
|
||||
pathExtension: self.pathExtension,
|
||||
diffing: self.diffing
|
||||
) { newValue in
|
||||
return .init { callback in
|
||||
transform(newValue).run { value in
|
||||
self.snapshot(value).run { snapshot in
|
||||
callback(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot strategy where the type being snapshot is also a diffable type.
|
||||
public typealias SimplySnapshotting<Format> = Snapshotting<Format, Format>
|
||||
|
||||
extension Snapshotting where Value == Format {
|
||||
public init(pathExtension: String?, diffing: Diffing<Format>) {
|
||||
self.init(
|
||||
pathExtension: pathExtension,
|
||||
diffing: diffing,
|
||||
snapshot: { $0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Snapshotting where Format == String {
|
||||
/// A snapshot strategy for comparing any structure based on a sanitized text dump.
|
||||
public static var dump: Snapshotting {
|
||||
return SimplySnapshotting.lines.pullback { snap($0) }
|
||||
}
|
||||
}
|
||||
|
||||
private func snap<T>(_ value: T, name: String? = nil, indent: Int = 0) -> String {
|
||||
let indentation = String(repeating: " ", count: indent)
|
||||
let mirror = Mirror(reflecting: value)
|
||||
var children = mirror.children
|
||||
let count = children.count
|
||||
let bullet = count == 0 ? "-" : "▿"
|
||||
|
||||
let description: String
|
||||
switch (value, mirror.displayStyle) {
|
||||
case (_, .collection?):
|
||||
description = count == 1 ? "1 element" : "\(count) elements"
|
||||
case (_, .dictionary?):
|
||||
description = count == 1 ? "1 key/value pair" : "\(count) key/value pairs"
|
||||
children = sort(children)
|
||||
case (_, .set?):
|
||||
description = count == 1 ? "1 member" : "\(count) members"
|
||||
children = sort(children)
|
||||
case (_, .tuple?):
|
||||
description = count == 1 ? "(1 element)" : "(\(count) elements)"
|
||||
case (_, .optional?):
|
||||
let subjectType = String(describing: mirror.subjectType)
|
||||
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
|
||||
description = count == 0 ? "\(subjectType).none" : "\(subjectType)"
|
||||
case (let value as AnySnapshotStringConvertible, _) where type(of: value).renderChildren:
|
||||
description = value.snapshotDescription
|
||||
case (let value as AnySnapshotStringConvertible, _):
|
||||
return "\(indentation)- \(name.map { "\($0): " } ?? "")\(value.snapshotDescription)\n"
|
||||
case (let value as CustomStringConvertible, _):
|
||||
description = value.description
|
||||
case (_, .class?), (_, .struct?):
|
||||
description = String(describing: mirror.subjectType)
|
||||
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
|
||||
children = sort(children)
|
||||
case (_, .enum?):
|
||||
let subjectType = String(describing: mirror.subjectType)
|
||||
.replacingOccurrences(of: " #\\d+", with: "", options: .regularExpression)
|
||||
description = count == 0 ? "\(subjectType).\(value)" : "\(subjectType)"
|
||||
case (let value, _):
|
||||
description = String(describing: value)
|
||||
}
|
||||
|
||||
let lines = ["\(indentation)\(bullet) \(name.map { "\($0): " } ?? "")\(description)\n"]
|
||||
+ children.map { snap($1, name: $0, indent: indent + 2) }
|
||||
|
||||
return lines.joined()
|
||||
}
|
||||
|
||||
private func sort(_ children: Mirror.Children) -> Mirror.Children {
|
||||
return .init(
|
||||
children
|
||||
.map({ (child: $0, snap: snap($0)) })
|
||||
.sorted(by: { $0.snap < $1.snap })
|
||||
.map({ $0.child })
|
||||
)
|
||||
}
|
||||
|
||||
/// A type with a customized snapshot dump representation.
|
||||
///
|
||||
/// Types that conform to the `AnySnapshotStringConvertible` protocol can provide their own representation to be used when converting an instance to a `dump`-based snapshot.
|
||||
public protocol AnySnapshotStringConvertible {
|
||||
/// Whether or not to dump child nodes (defaults to `false`).
|
||||
static var renderChildren: Bool { get }
|
||||
|
||||
/// A textual snapshot dump representation of this instance.
|
||||
var snapshotDescription: String { get }
|
||||
}
|
||||
|
||||
extension AnySnapshotStringConvertible {
|
||||
public static var renderChildren: Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension Character: AnySnapshotStringConvertible {
|
||||
public var snapshotDescription: String {
|
||||
return self.debugDescription
|
||||
}
|
||||
}
|
||||
|
||||
extension Data: AnySnapshotStringConvertible {
|
||||
public var snapshotDescription: String {
|
||||
return self.debugDescription
|
||||
}
|
||||
}
|
||||
|
||||
extension Date: AnySnapshotStringConvertible {
|
||||
public var snapshotDescription: String {
|
||||
return snapshotDateFormatter.string(from: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension NSObject: AnySnapshotStringConvertible {
|
||||
#if canImport(ObjectiveC)
|
||||
@objc open var snapshotDescription: String {
|
||||
return purgePointers(self.debugDescription)
|
||||
}
|
||||
#else
|
||||
open var snapshotDescription: String {
|
||||
return purgePointers(self.debugDescription)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension String: AnySnapshotStringConvertible {
|
||||
public var snapshotDescription: String {
|
||||
return self.debugDescription
|
||||
}
|
||||
}
|
||||
|
||||
extension Substring: AnySnapshotStringConvertible {
|
||||
public var snapshotDescription: String {
|
||||
return self.debugDescription
|
||||
}
|
||||
}
|
||||
|
||||
extension URL: AnySnapshotStringConvertible {
|
||||
public var snapshotDescription: String {
|
||||
return self.debugDescription
|
||||
}
|
||||
}
|
||||
|
||||
private let snapshotDateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
||||
return formatter
|
||||
}()
|
||||
|
||||
func purgePointers(_ string: String) -> String {
|
||||
return string.replacingOccurrences(of: ":?\\s*0x[\\da-f]+(\\s*)", with: "$1", options: .regularExpression)
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
|
||||
extension Snapshotting where Value == CALayer, Format == NSImage {
|
||||
/// A snapshot strategy for comparing layers based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image(precision: 1)
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing layers based on pixel equality.
|
||||
///
|
||||
/// - Parameter precision: The percentage of pixels that must match.
|
||||
public static func image(precision: Float) -> Snapshotting {
|
||||
return SimplySnapshotting.image(precision: precision).pullback { layer in
|
||||
let image = NSImage(size: layer.bounds.size)
|
||||
image.lockFocus()
|
||||
let context = NSGraphicsContext.current!.cgContext
|
||||
layer.setNeedsLayout()
|
||||
layer.layoutIfNeeded()
|
||||
layer.render(in: context)
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
}
|
||||
}
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
|
||||
extension Snapshotting where Value == CALayer, Format == UIImage {
|
||||
/// A snapshot strategy for comparing layers based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing layers based on pixel equality.
|
||||
///
|
||||
/// - Parameter precision: The percentage of pixels that must match.
|
||||
public static func image(precision: Float = 1, traits: UITraitCollection = .init())
|
||||
-> Snapshotting {
|
||||
return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).pullback { layer in
|
||||
renderer(bounds: layer.bounds, for: traits).image { ctx in
|
||||
layer.setNeedsLayout()
|
||||
layer.layoutIfNeeded()
|
||||
layer.render(in: ctx.cgContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
|
||||
extension Snapshotting where Value == CGPath, Format == NSImage {
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
///
|
||||
/// - Parameter precision: The percentage of pixels that must match.
|
||||
public static func image(precision: Float = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting {
|
||||
return SimplySnapshotting.image(precision: precision).pullback { path in
|
||||
let bounds = path.boundingBoxOfPath
|
||||
var transform = CGAffineTransform(translationX: -bounds.origin.x, y: -bounds.origin.y)
|
||||
let path = path.copy(using: &transform)!
|
||||
|
||||
let image = NSImage(size: bounds.size)
|
||||
image.lockFocus()
|
||||
let context = NSGraphicsContext.current!.cgContext
|
||||
|
||||
context.addPath(path)
|
||||
context.drawPath(using: drawingMode)
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
}
|
||||
}
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
|
||||
extension Snapshotting where Value == CGPath, Format == UIImage {
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
///
|
||||
/// - Parameter precision: The percentage of pixels that must match.
|
||||
public static func image(precision: Float = 1, scale: CGFloat = 1, drawingMode: CGPathDrawingMode = .eoFill) -> Snapshotting {
|
||||
return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in
|
||||
let bounds = path.boundingBoxOfPath
|
||||
let format: UIGraphicsImageRendererFormat
|
||||
if #available(iOS 11.0, tvOS 11.0, *) {
|
||||
format = UIGraphicsImageRendererFormat.preferred()
|
||||
} else {
|
||||
format = UIGraphicsImageRendererFormat.default()
|
||||
}
|
||||
format.scale = scale
|
||||
return UIGraphicsImageRenderer(bounds: bounds, format: format).image { ctx in
|
||||
let cgContext = ctx.cgContext
|
||||
cgContext.addPath(path)
|
||||
cgContext.drawPath(using: drawingMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS) || os(iOS) || os(tvOS)
|
||||
@available(iOS 11.0, OSX 10.13, tvOS 11.0, *)
|
||||
extension Snapshotting where Value == CGPath, Format == String {
|
||||
/// A snapshot strategy for comparing bezier paths based on element descriptions.
|
||||
public static var elementsDescription: Snapshotting {
|
||||
return .elementsDescription(numberFormatter: defaultNumberFormatter)
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing bezier paths based on element descriptions.
|
||||
///
|
||||
/// - Parameter numberFormatter: The number formatter used for formatting points.
|
||||
public static func elementsDescription(numberFormatter: NumberFormatter) -> Snapshotting {
|
||||
let namesByType: [CGPathElementType: String] = [
|
||||
.moveToPoint: "MoveTo",
|
||||
.addLineToPoint: "LineTo",
|
||||
.addQuadCurveToPoint: "QuadCurveTo",
|
||||
.addCurveToPoint: "CurveTo",
|
||||
.closeSubpath: "Close",
|
||||
]
|
||||
|
||||
let numberOfPointsByType: [CGPathElementType: Int] = [
|
||||
.moveToPoint: 1,
|
||||
.addLineToPoint: 1,
|
||||
.addQuadCurveToPoint: 2,
|
||||
.addCurveToPoint: 3,
|
||||
.closeSubpath: 0,
|
||||
]
|
||||
|
||||
return SimplySnapshotting.lines.pullback { path in
|
||||
var string: String = ""
|
||||
|
||||
path.applyWithBlock { elementPointer in
|
||||
let element = elementPointer.pointee
|
||||
let name = namesByType[element.type] ?? "Unknown"
|
||||
|
||||
if element.type == .moveToPoint && !string.isEmpty {
|
||||
string += "\n"
|
||||
}
|
||||
|
||||
string += name
|
||||
|
||||
if let numberOfPoints = numberOfPointsByType[element.type] {
|
||||
let points = UnsafeBufferPointer(start: element.points, count: numberOfPoints)
|
||||
string += " " + points.map { point in
|
||||
let x = numberFormatter.string(from: point.x as NSNumber)!
|
||||
let y = numberFormatter.string(from: point.y as NSNumber)!
|
||||
return "(\(x), \(y))"
|
||||
}.joined(separator: " ")
|
||||
}
|
||||
|
||||
string += "\n"
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let defaultNumberFormatter: NumberFormatter = {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.minimumFractionDigits = 1
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
return numberFormatter
|
||||
}()
|
||||
#endif
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
extension Snapshotting where Value: CaseIterable, Format == String {
|
||||
/// A strategy for snapshotting the output for every input of a function. The format of the snapshot
|
||||
/// is a comma-separated value (CSV) file that shows the mapping of inputs to outputs.
|
||||
///
|
||||
/// Parameter witness: A snapshotting value on the output of the function to be snapshot.
|
||||
/// Returns: A snapshot strategy on functions (Value) -> A that feeds every possible input into the
|
||||
/// function and records the output into a CSV file.
|
||||
public static func `func`<A>(into witness: Snapshotting<A, Format>) -> Snapshotting<(Value) -> A, Format> {
|
||||
var snapshotting = Snapshotting<String, String>.lines.asyncPullback { (f: (Value) -> A) in
|
||||
Value.allCases.map { input in
|
||||
witness.snapshot(f(input))
|
||||
.map { (input, $0) }
|
||||
}
|
||||
.sequence()
|
||||
.map { rows in
|
||||
rows.map { "\"\($0)\",\"\($1)\"" }
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
snapshotting.pathExtension = "csv"
|
||||
|
||||
return snapshotting
|
||||
}
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Snapshotting where Value: Encodable, Format == String {
|
||||
/// A snapshot strategy for comparing encodable structures based on their JSON representation.
|
||||
@available(iOS 11.0, macOS 10.13, tvOS 11.0, *)
|
||||
public static var json: Snapshotting {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
return .json(encoder)
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing encodable structures based on their JSON representation.
|
||||
///
|
||||
/// - Parameter encoder: A JSON encoder.
|
||||
public static func json(_ encoder: JSONEncoder) -> Snapshotting {
|
||||
var snapshotting = SimplySnapshotting.lines.pullback { (encodable: Value) in
|
||||
try! String(decoding: encoder.encode(encodable), as: UTF8.self)
|
||||
}
|
||||
snapshotting.pathExtension = "json"
|
||||
return snapshotting
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing encodable structures based on their property list representation.
|
||||
public static var plist: Snapshotting {
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .xml
|
||||
return .plist(encoder)
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing encodable structures based on their property list representation.
|
||||
///
|
||||
/// - Parameter encoder: A property list encoder.
|
||||
public static func plist(_ encoder: PropertyListEncoder) -> Snapshotting {
|
||||
var snapshotting = SimplySnapshotting.lines.pullback { (encodable: Value) in
|
||||
try! String(decoding: encoder.encode(encodable), as: UTF8.self)
|
||||
}
|
||||
snapshotting.pathExtension = "plist"
|
||||
return snapshotting
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
extension Snapshotting where Value == Data, Format == Data {
|
||||
public static var data: Snapshotting {
|
||||
return .init(
|
||||
pathExtension: nil,
|
||||
diffing: .init(toData: { $0 }, fromData: { $0 }) { old, new in
|
||||
guard old != new else { return nil }
|
||||
let message = old.count == new.count
|
||||
? "Expected data to match"
|
||||
: "Expected \(new) to match \(old)"
|
||||
return (message, [])
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
extension Snapshotting where Format == String {
|
||||
/// A snapshot strategy that captures a value's textual description from `String`'s `init(description:)`
|
||||
/// initializer.
|
||||
public static var description: Snapshotting {
|
||||
return SimplySnapshotting.lines.pullback(String.init(describing:))
|
||||
}
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
|
||||
extension Snapshotting where Value == NSBezierPath, Format == NSImage {
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
///
|
||||
/// - Parameter precision: The percentage of pixels that must match.
|
||||
public static func image(precision: Float = 1) -> Snapshotting {
|
||||
return SimplySnapshotting.image(precision: precision).pullback { path in
|
||||
// Move path info frame:
|
||||
let bounds = path.bounds
|
||||
let transform = AffineTransform(translationByX: -bounds.origin.x, byY: -bounds.origin.y)
|
||||
path.transform(using: transform)
|
||||
|
||||
let image = NSImage(size: path.bounds.size)
|
||||
image.lockFocus()
|
||||
path.fill()
|
||||
image.unlockFocus()
|
||||
return image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == NSBezierPath, Format == String {
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
@available(iOS 11.0, *)
|
||||
public static var elementsDescription: Snapshotting {
|
||||
return .elementsDescription(numberFormatter: defaultNumberFormatter)
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
///
|
||||
/// - Parameter numberFormatter: The number formatter used for formatting points.
|
||||
@available(iOS 11.0, *)
|
||||
public static func elementsDescription(numberFormatter: NumberFormatter) -> Snapshotting {
|
||||
let namesByType: [NSBezierPath.ElementType: String] = [
|
||||
.moveTo: "MoveTo",
|
||||
.lineTo: "LineTo",
|
||||
.curveTo: "CurveTo",
|
||||
.closePath: "Close",
|
||||
]
|
||||
|
||||
let numberOfPointsByType: [NSBezierPath.ElementType: Int] = [
|
||||
.moveTo: 1,
|
||||
.lineTo: 1,
|
||||
.curveTo: 3,
|
||||
.closePath: 0,
|
||||
]
|
||||
|
||||
return SimplySnapshotting.lines.pullback { path in
|
||||
var string: String = ""
|
||||
|
||||
var elementPoints = [CGPoint](repeating: .zero, count: 3)
|
||||
for elementIndex in 0..<path.elementCount {
|
||||
let elementType = path.element(at: elementIndex, associatedPoints: &elementPoints)
|
||||
let name = namesByType[elementType] ?? "Unknown"
|
||||
|
||||
if elementType == .moveTo && !string.isEmpty {
|
||||
string += "\n"
|
||||
}
|
||||
|
||||
string += name
|
||||
|
||||
if let numberOfPoints = numberOfPointsByType[elementType] {
|
||||
let points = elementPoints[0..<numberOfPoints]
|
||||
string += " " + points.map { point in
|
||||
let x = numberFormatter.string(from: point.x as NSNumber)!
|
||||
let y = numberFormatter.string(from: point.y as NSNumber)!
|
||||
return "(\(x), \(y))"
|
||||
}.joined(separator: " ")
|
||||
}
|
||||
|
||||
string += "\n"
|
||||
}
|
||||
|
||||
return string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let defaultNumberFormatter: NumberFormatter = {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.minimumFractionDigits = 1
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
return numberFormatter
|
||||
}()
|
||||
#endif
|
||||
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
import XCTest
|
||||
|
||||
extension Diffing where Value == NSImage {
|
||||
/// A pixel-diffing strategy for NSImage's which requires a 100% match.
|
||||
public static let image = Diffing.image(precision: 1)
|
||||
|
||||
/// A pixel-diffing strategy for NSImage that allows customizing how precise the matching must be.
|
||||
///
|
||||
/// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels.
|
||||
/// - Returns: A new diffing strategy.
|
||||
public static func image(precision: Float) -> Diffing {
|
||||
return .init(
|
||||
toData: { NSImagePNGRepresentation($0)! },
|
||||
fromData: { NSImage(data: $0)! }
|
||||
) { old, new in
|
||||
guard !compare(old, new, precision: precision) else { return nil }
|
||||
let difference = SnapshotTesting.diff(old, new)
|
||||
let message = new.size == old.size
|
||||
? "Newly-taken snapshot does not match reference."
|
||||
: "Newly-taken snapshot@\(new.size) does not match reference@\(old.size)."
|
||||
return (
|
||||
message,
|
||||
[XCTAttachment(image: old), XCTAttachment(image: new), XCTAttachment(image: difference)]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == NSImage, Format == NSImage {
|
||||
/// A snapshot strategy for comparing images based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image(precision: 1)
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing images based on pixel equality.
|
||||
///
|
||||
/// - Parameter precision: The percentage of pixels that must match.
|
||||
public static func image(precision: Float) -> Snapshotting {
|
||||
return .init(
|
||||
pathExtension: "png",
|
||||
diffing: .image(precision: precision)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func NSImagePNGRepresentation(_ image: NSImage) -> Data? {
|
||||
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
|
||||
let rep = NSBitmapImageRep(cgImage: cgImage)
|
||||
rep.size = image.size
|
||||
return rep.representation(using: .png, properties: [:])
|
||||
}
|
||||
|
||||
private func compare(_ old: NSImage, _ new: NSImage, precision: Float) -> Bool {
|
||||
guard let oldCgImage = old.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false }
|
||||
guard let newCgImage = new.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false }
|
||||
guard oldCgImage.width != 0 else { return false }
|
||||
guard newCgImage.width != 0 else { return false }
|
||||
guard oldCgImage.width == newCgImage.width else { return false }
|
||||
guard oldCgImage.height != 0 else { return false }
|
||||
guard newCgImage.height != 0 else { return false }
|
||||
guard oldCgImage.height == newCgImage.height else { return false }
|
||||
guard let oldContext = context(for: oldCgImage) else { return false }
|
||||
guard let newContext = context(for: newCgImage) else { return false }
|
||||
guard let oldData = oldContext.data else { return false }
|
||||
guard let newData = newContext.data else { return false }
|
||||
let byteCount = oldContext.height * oldContext.bytesPerRow
|
||||
if memcmp(oldData, newData, byteCount) == 0 { return true }
|
||||
let newer = NSImage(data: NSImagePNGRepresentation(new)!)!
|
||||
guard let newerCgImage = newer.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false }
|
||||
guard let newerContext = context(for: newerCgImage) else { return false }
|
||||
guard let newerData = newerContext.data else { return false }
|
||||
if memcmp(oldData, newerData, byteCount) == 0 { return true }
|
||||
if precision >= 1 { return false }
|
||||
let oldRep = NSBitmapImageRep(cgImage: oldCgImage)
|
||||
let newRep = NSBitmapImageRep(cgImage: newerCgImage)
|
||||
var differentPixelCount = 0
|
||||
let pixelCount = oldRep.pixelsWide * oldRep.pixelsHigh
|
||||
let threshold = (1 - precision) * Float(pixelCount)
|
||||
let p1: UnsafeMutablePointer<UInt8> = oldRep.bitmapData!
|
||||
let p2: UnsafeMutablePointer<UInt8> = newRep.bitmapData!
|
||||
for offset in 0 ..< pixelCount * 4 {
|
||||
if p1[offset] != p2[offset] {
|
||||
differentPixelCount += 1
|
||||
}
|
||||
if Float(differentPixelCount) > threshold { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func context(for cgImage: CGImage) -> CGContext? {
|
||||
guard
|
||||
let space = cgImage.colorSpace,
|
||||
let context = CGContext(
|
||||
data: nil,
|
||||
width: cgImage.width,
|
||||
height: cgImage.height,
|
||||
bitsPerComponent: cgImage.bitsPerComponent,
|
||||
bytesPerRow: cgImage.bytesPerRow,
|
||||
space: space,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
)
|
||||
else { return nil }
|
||||
|
||||
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
|
||||
return context
|
||||
}
|
||||
|
||||
private func diff(_ old: NSImage, _ new: NSImage) -> NSImage {
|
||||
let oldCiImage = CIImage(cgImage: old.cgImage(forProposedRect: nil, context: nil, hints: nil)!)
|
||||
let newCiImage = CIImage(cgImage: new.cgImage(forProposedRect: nil, context: nil, hints: nil)!)
|
||||
let differenceFilter = CIFilter(name: "CIDifferenceBlendMode")!
|
||||
differenceFilter.setValue(oldCiImage, forKey: kCIInputImageKey)
|
||||
differenceFilter.setValue(newCiImage, forKey: kCIInputBackgroundImageKey)
|
||||
let maxSize = CGSize(
|
||||
width: max(old.size.width, new.size.width),
|
||||
height: max(old.size.height, new.size.height)
|
||||
)
|
||||
let rep = NSCIImageRep(ciImage: differenceFilter.outputImage!)
|
||||
let difference = NSImage(size: maxSize)
|
||||
difference.addRepresentation(rep)
|
||||
return difference
|
||||
}
|
||||
#endif
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
|
||||
extension Snapshotting where Value == NSView, Format == NSImage {
|
||||
/// A snapshot strategy for comparing views based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing views based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: A view size override.
|
||||
public static func image(precision: Float = 1, size: CGSize? = nil) -> Snapshotting {
|
||||
return SimplySnapshotting.image(precision: precision).asyncPullback { view in
|
||||
let initialSize = view.frame.size
|
||||
if let size = size { view.frame.size = size }
|
||||
guard view.frame.width > 0, view.frame.height > 0 else {
|
||||
fatalError("View not renderable to image at size \(view.frame.size)")
|
||||
}
|
||||
return view.snapshot ?? Async { callback in
|
||||
addImagesForRenderedViews(view).sequence().run { views in
|
||||
let bitmapRep = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
|
||||
view.cacheDisplay(in: view.bounds, to: bitmapRep)
|
||||
let image = NSImage(size: view.bounds.size)
|
||||
image.addRepresentation(bitmapRep)
|
||||
callback(image)
|
||||
views.forEach { $0.removeFromSuperview() }
|
||||
view.frame.size = initialSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == NSView, Format == String {
|
||||
/// A snapshot strategy for comparing views based on a recursive description of their properties and hierarchies.
|
||||
public static var recursiveDescription: Snapshotting<NSView, String> {
|
||||
return SimplySnapshotting.lines.pullback { view in
|
||||
return purgePointers(
|
||||
view.perform(Selector(("_subtreeDescription"))).retain().takeUnretainedValue()
|
||||
as! String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
|
||||
extension Snapshotting where Value == NSViewController, Format == NSImage {
|
||||
/// A snapshot strategy for comparing view controller views based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing view controller views based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: A view size override.
|
||||
public static func image(precision: Float = 1, size: CGSize? = nil) -> Snapshotting {
|
||||
return Snapshotting<NSView, NSImage>.image(precision: precision, size: size).pullback { $0.view }
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == NSViewController, Format == String {
|
||||
/// A snapshot strategy for comparing view controller views based on a recursive description of their properties and hierarchies.
|
||||
public static var recursiveDescription: Snapshotting {
|
||||
return Snapshotting<NSView, String>.recursiveDescription.pullback { $0.view }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
#if os(iOS) || os(macOS) || os(tvOS)
|
||||
import SceneKit
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
extension Snapshotting where Value == SCNScene, Format == NSImage {
|
||||
/// A snapshot strategy for comparing SceneKit scenes based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: The size of the scene.
|
||||
public static func image(precision: Float = 1, size: CGSize) -> Snapshotting {
|
||||
return .scnScene(precision: precision, size: size)
|
||||
}
|
||||
}
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
extension Snapshotting where Value == SCNScene, Format == UIImage {
|
||||
/// A snapshot strategy for comparing SceneKit scenes based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: The size of the scene.
|
||||
public static func image(precision: Float = 1, size: CGSize) -> Snapshotting {
|
||||
return .scnScene(precision: precision, size: size)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
fileprivate extension Snapshotting where Value == SCNScene, Format == Image {
|
||||
static func scnScene(precision: Float, size: CGSize) -> Snapshotting {
|
||||
return Snapshotting<View, Image>.image(precision: precision).pullback { scene in
|
||||
let view = SCNView(frame: .init(x: 0, y: 0, width: size.width, height: size.height))
|
||||
view.scene = scene
|
||||
return view
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
#if os(iOS) || os(macOS) || os(tvOS)
|
||||
import SpriteKit
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
extension Snapshotting where Value == SKScene, Format == NSImage {
|
||||
/// A snapshot strategy for comparing SpriteKit scenes based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: The size of the scene.
|
||||
public static func image(precision: Float = 1, size: CGSize) -> Snapshotting {
|
||||
return .skScene(precision: precision, size: size)
|
||||
}
|
||||
}
|
||||
#elseif os(iOS) || os(tvOS)
|
||||
extension Snapshotting where Value == SKScene, Format == UIImage {
|
||||
/// A snapshot strategy for comparing SpriteKit scenes based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: The size of the scene.
|
||||
public static func image(precision: Float = 1, size: CGSize) -> Snapshotting {
|
||||
return .skScene(precision: precision, size: size)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
fileprivate extension Snapshotting where Value == SKScene, Format == Image {
|
||||
static func skScene(precision: Float, size: CGSize) -> Snapshotting {
|
||||
return Snapshotting<View, Image>.image(precision: precision).pullback { scene in
|
||||
let view = SKView(frame: .init(x: 0, y: 0, width: size.width, height: size.height))
|
||||
view.presentScene(scene)
|
||||
return view
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
extension Snapshotting where Value == String, Format == String {
|
||||
/// A snapshot strategy for comparing strings based on equality.
|
||||
public static let lines = Snapshotting(pathExtension: "txt", diffing: .lines)
|
||||
}
|
||||
|
||||
extension Diffing where Value == String {
|
||||
/// A line-diffing strategy for UTF-8 text.
|
||||
public static let lines = Diffing(
|
||||
toData: { Data($0.utf8) },
|
||||
fromData: { String(decoding: $0, as: UTF8.self) }
|
||||
) { old, new in
|
||||
guard old != new else { return nil }
|
||||
let hunks = chunk(diff: SnapshotTesting.diff(
|
||||
old.split(separator: "\n", omittingEmptySubsequences: false).map(String.init),
|
||||
new.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
|
||||
))
|
||||
let failure = hunks
|
||||
.flatMap { [$0.patchMark] + $0.lines }
|
||||
.joined(separator: "\n")
|
||||
let attachment = XCTAttachment(data: Data(failure.utf8), uniformTypeIdentifier: "public.patch-file")
|
||||
return (failure, [attachment])
|
||||
}
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
#if canImport(SwiftUI)
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// The size constraint for a snapshot (similar to `PreviewLayout`).
|
||||
public enum SwiftUISnapshotLayout {
|
||||
#if os(iOS) || os(tvOS)
|
||||
/// Center the view in a device container described by`config`.
|
||||
case device(config: ViewImageConfig)
|
||||
#endif
|
||||
/// Center the view in a fixed size container.
|
||||
case fixed(width: CGFloat, height: CGFloat)
|
||||
/// Fit the view to the ideal size that fits its content.
|
||||
case sizeThatFits
|
||||
}
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
@available(iOS 13.0, tvOS 13.0, *)
|
||||
extension Snapshotting where Value: SwiftUI.View, Format == UIImage {
|
||||
|
||||
/// A snapshot strategy for comparing SwiftUI Views based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing SwiftUI Views based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets.
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: A view size override.
|
||||
/// - traits: A trait collection override.
|
||||
public static func image(
|
||||
drawHierarchyInKeyWindow: Bool = false,
|
||||
precision: Float = 1,
|
||||
layout: SwiftUISnapshotLayout = .sizeThatFits,
|
||||
traits: UITraitCollection = .init()
|
||||
)
|
||||
-> Snapshotting {
|
||||
let config: ViewImageConfig
|
||||
|
||||
switch layout {
|
||||
#if os(iOS) || os(tvOS)
|
||||
case let .device(config: deviceConfig):
|
||||
config = deviceConfig
|
||||
#endif
|
||||
case .sizeThatFits:
|
||||
config = .init(safeArea: .zero, size: nil, traits: traits)
|
||||
case let .fixed(width: width, height: height):
|
||||
let size = CGSize(width: width, height: height)
|
||||
config = .init(safeArea: .zero, size: size, traits: traits)
|
||||
}
|
||||
|
||||
return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in
|
||||
var config = config
|
||||
|
||||
let controller: UIViewController
|
||||
|
||||
if config.size != nil {
|
||||
controller = UIHostingController.init(
|
||||
rootView: view
|
||||
)
|
||||
} else {
|
||||
let hostingController = UIHostingController.init(rootView: view)
|
||||
|
||||
let maxSize = CGSize(width: 0.0, height: 0.0)
|
||||
config.size = hostingController.sizeThatFits(in: maxSize)
|
||||
|
||||
controller = hostingController
|
||||
}
|
||||
|
||||
return snapshotView(
|
||||
config: config,
|
||||
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
|
||||
traits: traits,
|
||||
view: controller.view,
|
||||
viewController: controller
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
|
||||
extension Snapshotting where Value == UIBezierPath, Format == UIImage {
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
///
|
||||
/// - Parameter precision: The percentage of pixels that must match.
|
||||
public static func image(precision: Float = 1, scale: CGFloat = 1) -> Snapshotting {
|
||||
return SimplySnapshotting.image(precision: precision, scale: scale).pullback { path in
|
||||
let bounds = path.bounds
|
||||
let format: UIGraphicsImageRendererFormat
|
||||
if #available(iOS 11.0, tvOS 11.0, *) {
|
||||
format = UIGraphicsImageRendererFormat.preferred()
|
||||
} else {
|
||||
format = UIGraphicsImageRendererFormat.default()
|
||||
}
|
||||
format.scale = scale
|
||||
return UIGraphicsImageRenderer(bounds: bounds, format: format).image { ctx in
|
||||
path.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 11.0, tvOS 11.0, *)
|
||||
extension Snapshotting where Value == UIBezierPath, Format == String {
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
public static var elementsDescription: Snapshotting {
|
||||
Snapshotting<CGPath, String>.elementsDescription.pullback { path in path.cgPath }
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing bezier paths based on pixel equality.
|
||||
///
|
||||
/// - Parameter numberFormatter: The number formatter used for formatting points.
|
||||
public static func elementsDescription(numberFormatter: NumberFormatter) -> Snapshotting {
|
||||
Snapshotting<CGPath, String>.elementsDescription(
|
||||
numberFormatter: numberFormatter
|
||||
).pullback { path in path.cgPath }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
extension Diffing where Value == UIImage {
|
||||
/// A pixel-diffing strategy for UIImage's which requires a 100% match.
|
||||
public static let image = Diffing.image(precision: 1, scale: nil)
|
||||
|
||||
/// A pixel-diffing strategy for UIImage that allows customizing how precise the matching must be.
|
||||
///
|
||||
/// - Parameter precision: A value between 0 and 1, where 1 means the images must match 100% of their pixels.
|
||||
/// - Parameter scale: Scale to use when loading the reference image from disk. If `nil` or the `UITraitCollection`s default value of `0.0`, the screens scale is used.
|
||||
/// - Returns: A new diffing strategy.
|
||||
public static func image(precision: Float, scale: CGFloat?) -> Diffing {
|
||||
let imageScale: CGFloat
|
||||
if let scale = scale, scale != 0.0 {
|
||||
imageScale = scale
|
||||
} else {
|
||||
imageScale = UIScreen.main.scale
|
||||
}
|
||||
|
||||
return Diffing(
|
||||
toData: { $0.pngData() ?? emptyImage().pngData()! },
|
||||
fromData: { UIImage(data: $0, scale: imageScale)! }
|
||||
) { old, new in
|
||||
guard !compare(old, new, precision: precision) else { return nil }
|
||||
let difference = SnapshotTesting.diff(old, new)
|
||||
let message = new.size == old.size
|
||||
? "Newly-taken snapshot does not match reference."
|
||||
: "Newly-taken snapshot@\(new.size) does not match reference@\(old.size)."
|
||||
let oldAttachment = XCTAttachment(image: old)
|
||||
oldAttachment.name = "reference"
|
||||
let newAttachment = XCTAttachment(image: new)
|
||||
newAttachment.name = "failure"
|
||||
let differenceAttachment = XCTAttachment(image: difference)
|
||||
differenceAttachment.name = "difference"
|
||||
return (
|
||||
message,
|
||||
[oldAttachment, newAttachment, differenceAttachment]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Used when the image size has no width or no height to generated the default empty image
|
||||
private static func emptyImage() -> UIImage {
|
||||
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 400, height: 80))
|
||||
label.backgroundColor = .red
|
||||
label.text = "Error: No image could be generated for this view as its size was zero. Please set an explicit size in the test."
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 3
|
||||
return label.asImage()
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == UIImage, Format == UIImage {
|
||||
/// A snapshot strategy for comparing images based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image(precision: 1, scale: nil)
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing images based on pixel equality.
|
||||
///
|
||||
/// - Parameter precision: The percentage of pixels that must match.
|
||||
/// - Parameter scale: The scale of the reference image stored on disk.
|
||||
public static func image(precision: Float, scale: CGFloat?) -> Snapshotting {
|
||||
return .init(
|
||||
pathExtension: "png",
|
||||
diffing: .image(precision: precision, scale: scale)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool {
|
||||
guard let oldCgImage = old.cgImage else { return false }
|
||||
guard let newCgImage = new.cgImage else { return false }
|
||||
guard oldCgImage.width != 0 else { return false }
|
||||
guard newCgImage.width != 0 else { return false }
|
||||
guard oldCgImage.width == newCgImage.width else { return false }
|
||||
guard oldCgImage.height != 0 else { return false }
|
||||
guard newCgImage.height != 0 else { return false }
|
||||
guard oldCgImage.height == newCgImage.height else { return false }
|
||||
// Values between images may differ due to padding to multiple of 64 bytes per row,
|
||||
// because of that a freshly taken view snapshot may differ from one stored as PNG.
|
||||
// At this point we're sure that size of both images is the same, so we can go with minimal `bytesPerRow` value
|
||||
// and use it to create contexts.
|
||||
let minBytesPerRow = min(oldCgImage.bytesPerRow, newCgImage.bytesPerRow)
|
||||
let byteCount = minBytesPerRow * oldCgImage.height
|
||||
|
||||
var oldBytes = [UInt8](repeating: 0, count: byteCount)
|
||||
guard let oldContext = context(for: oldCgImage, bytesPerRow: minBytesPerRow, data: &oldBytes) else { return false }
|
||||
guard let oldData = oldContext.data else { return false }
|
||||
if let newContext = context(for: newCgImage, bytesPerRow: minBytesPerRow), let newData = newContext.data {
|
||||
if memcmp(oldData, newData, byteCount) == 0 { return true }
|
||||
}
|
||||
let newer = UIImage(data: new.pngData()!)!
|
||||
guard let newerCgImage = newer.cgImage else { return false }
|
||||
var newerBytes = [UInt8](repeating: 0, count: byteCount)
|
||||
guard let newerContext = context(for: newerCgImage, bytesPerRow: minBytesPerRow, data: &newerBytes) else { return false }
|
||||
guard let newerData = newerContext.data else { return false }
|
||||
if memcmp(oldData, newerData, byteCount) == 0 { return true }
|
||||
if precision >= 1 { return false }
|
||||
var differentPixelCount = 0
|
||||
let threshold = 1 - precision
|
||||
for byte in 0..<byteCount {
|
||||
if oldBytes[byte] != newerBytes[byte] { differentPixelCount += 1 }
|
||||
if Float(differentPixelCount) / Float(byteCount) > threshold { return false}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func context(for cgImage: CGImage, bytesPerRow: Int, data: UnsafeMutableRawPointer? = nil) -> CGContext? {
|
||||
guard
|
||||
let space = cgImage.colorSpace,
|
||||
let context = CGContext(
|
||||
data: data,
|
||||
width: cgImage.width,
|
||||
height: cgImage.height,
|
||||
bitsPerComponent: cgImage.bitsPerComponent,
|
||||
bytesPerRow: bytesPerRow,
|
||||
space: space,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
)
|
||||
else { return nil }
|
||||
|
||||
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
|
||||
return context
|
||||
}
|
||||
|
||||
private func diff(_ old: UIImage, _ new: UIImage) -> UIImage {
|
||||
let width = max(old.size.width, new.size.width)
|
||||
let height = max(old.size.height, new.size.height)
|
||||
UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), true, 0)
|
||||
new.draw(at: .zero)
|
||||
old.draw(at: .zero, blendMode: .difference, alpha: 1)
|
||||
let differenceImage = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
return differenceImage
|
||||
}
|
||||
#endif
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
|
||||
extension Snapshotting where Value == UIView, Format == UIImage {
|
||||
/// A snapshot strategy for comparing views based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing views based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets.
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: A view size override.
|
||||
/// - traits: A trait collection override.
|
||||
public static func image(
|
||||
drawHierarchyInKeyWindow: Bool = false,
|
||||
precision: Float = 1,
|
||||
size: CGSize? = nil,
|
||||
traits: UITraitCollection = .init()
|
||||
)
|
||||
-> Snapshotting {
|
||||
|
||||
return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { view in
|
||||
snapshotView(
|
||||
config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: .init()),
|
||||
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
|
||||
traits: traits,
|
||||
view: view,
|
||||
viewController: .init()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == UIView, Format == String {
|
||||
/// A snapshot strategy for comparing views based on a recursive description of their properties and hierarchies.
|
||||
public static var recursiveDescription: Snapshotting {
|
||||
return Snapshotting.recursiveDescription()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing views based on a recursive description of their properties and hierarchies.
|
||||
public static func recursiveDescription(
|
||||
size: CGSize? = nil,
|
||||
traits: UITraitCollection = .init()
|
||||
)
|
||||
-> Snapshotting<UIView, String> {
|
||||
return SimplySnapshotting.lines.pullback { view in
|
||||
let dispose = prepareView(
|
||||
config: .init(safeArea: .zero, size: size ?? view.frame.size, traits: traits),
|
||||
drawHierarchyInKeyWindow: false,
|
||||
traits: .init(),
|
||||
view: view,
|
||||
viewController: .init()
|
||||
)
|
||||
defer { dispose() }
|
||||
return purgePointers(
|
||||
view.perform(Selector(("recursiveDescription"))).retain().takeUnretainedValue()
|
||||
as! String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
#if os(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
|
||||
extension Snapshotting where Value == UIViewController, Format == UIImage {
|
||||
/// A snapshot strategy for comparing view controller views based on pixel equality.
|
||||
public static var image: Snapshotting {
|
||||
return .image()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing view controller views based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - config: A set of device configuration settings.
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: A view size override.
|
||||
/// - traits: A trait collection override.
|
||||
public static func image(
|
||||
on config: ViewImageConfig,
|
||||
precision: Float = 1,
|
||||
size: CGSize? = nil,
|
||||
traits: UITraitCollection = .init()
|
||||
)
|
||||
-> Snapshotting {
|
||||
|
||||
return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { viewController in
|
||||
snapshotView(
|
||||
config: size.map { .init(safeArea: config.safeArea, size: $0, traits: config.traits) } ?? config,
|
||||
drawHierarchyInKeyWindow: false,
|
||||
traits: traits,
|
||||
view: viewController.view,
|
||||
viewController: viewController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing view controller views based on pixel equality.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets.
|
||||
/// - precision: The percentage of pixels that must match.
|
||||
/// - size: A view size override.
|
||||
/// - traits: A trait collection override.
|
||||
public static func image(
|
||||
drawHierarchyInKeyWindow: Bool = false,
|
||||
precision: Float = 1,
|
||||
size: CGSize? = nil,
|
||||
traits: UITraitCollection = .init()
|
||||
)
|
||||
-> Snapshotting {
|
||||
|
||||
return SimplySnapshotting.image(precision: precision, scale: traits.displayScale).asyncPullback { viewController in
|
||||
snapshotView(
|
||||
config: .init(safeArea: .zero, size: size, traits: traits),
|
||||
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
|
||||
traits: .init(),
|
||||
view: viewController.view,
|
||||
viewController: viewController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Snapshotting where Value == UIViewController, Format == String {
|
||||
/// A snapshot strategy for comparing view controllers based on their embedded controller hierarchy.
|
||||
public static var hierarchy: Snapshotting {
|
||||
return Snapshotting<String, String>.lines.pullback { viewController in
|
||||
let dispose = prepareView(
|
||||
config: .init(),
|
||||
drawHierarchyInKeyWindow: false,
|
||||
traits: .init(),
|
||||
view: viewController.view,
|
||||
viewController: viewController
|
||||
)
|
||||
defer { dispose() }
|
||||
return purgePointers(
|
||||
viewController.perform(Selector(("_printHierarchy"))).retain().takeUnretainedValue() as! String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing view controller views based on a recursive description of their properties and hierarchies.
|
||||
public static var recursiveDescription: Snapshotting {
|
||||
return Snapshotting.recursiveDescription()
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing view controller views based on a recursive description of their properties and hierarchies.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - config: A set of device configuration settings.
|
||||
/// - size: A view size override.
|
||||
/// - traits: A trait collection override.
|
||||
public static func recursiveDescription(
|
||||
on config: ViewImageConfig = .init(),
|
||||
size: CGSize? = nil,
|
||||
traits: UITraitCollection = .init()
|
||||
)
|
||||
-> Snapshotting<UIViewController, String> {
|
||||
return SimplySnapshotting.lines.pullback { viewController in
|
||||
let dispose = prepareView(
|
||||
config: .init(safeArea: config.safeArea, size: size ?? config.size, traits: config.traits),
|
||||
drawHierarchyInKeyWindow: false,
|
||||
traits: traits,
|
||||
view: viewController.view,
|
||||
viewController: viewController
|
||||
)
|
||||
defer { dispose() }
|
||||
return purgePointers(
|
||||
viewController.view.perform(Selector(("recursiveDescription"))).retain().takeUnretainedValue()
|
||||
as! String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
extension Snapshotting where Value == URLRequest, Format == String {
|
||||
/// A snapshot strategy for comparing requests based on raw equality.
|
||||
public static let raw = Snapshotting.raw(pretty: false)
|
||||
|
||||
/// A snapshot strategy for comparing requests based on raw equality.
|
||||
///
|
||||
/// - Parameter pretty: Attempts to pretty print the body of the request (supports JSON).
|
||||
public static func raw(pretty: Bool) -> Snapshotting {
|
||||
return SimplySnapshotting.lines.pullback { (request: URLRequest) in
|
||||
let method = "\(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "(null)")"
|
||||
|
||||
let headers = (request.allHTTPHeaderFields ?? [:])
|
||||
.map { key, value in "\(key): \(value)" }
|
||||
.sorted()
|
||||
|
||||
let body: [String]
|
||||
do {
|
||||
if pretty, #available(iOS 11.0, macOS 10.13, tvOS 11.0, *) {
|
||||
body = try request.httpBody
|
||||
.map { try JSONSerialization.jsonObject(with: $0, options: []) }
|
||||
.map { try JSONSerialization.data(withJSONObject: $0, options: [.prettyPrinted, .sortedKeys]) }
|
||||
.map { ["\n\(String(decoding: $0, as: UTF8.self))"] }
|
||||
?? []
|
||||
} else {
|
||||
throw NSError(domain: "co.pointfree.Never", code: 1, userInfo: nil)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
body = request.httpBody
|
||||
.map { ["\n\(String(decoding: $0, as: UTF8.self))"] }
|
||||
?? []
|
||||
}
|
||||
|
||||
return ([method] + headers + body).joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot strategy for comparing requests based on a cURL representation.
|
||||
public static let curl = SimplySnapshotting.lines.pullback { (request: URLRequest) in
|
||||
|
||||
var components = ["curl"]
|
||||
|
||||
// HTTP Method
|
||||
let httpMethod = request.httpMethod!
|
||||
switch httpMethod {
|
||||
case "GET": break
|
||||
case "HEAD": components.append("--head")
|
||||
default: components.append("--request \(httpMethod)")
|
||||
}
|
||||
|
||||
// Headers
|
||||
if let headers = request.allHTTPHeaderFields {
|
||||
for field in headers.keys.sorted() where field != "Cookie" {
|
||||
let escapedValue = headers[field]!.replacingOccurrences(of: "\"", with: "\\\"")
|
||||
components.append("--header \"\(field): \(escapedValue)\"")
|
||||
}
|
||||
}
|
||||
|
||||
// Body
|
||||
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("--data \"\(escapedBody)\"")
|
||||
}
|
||||
|
||||
// Cookies
|
||||
if let cookie = request.allHTTPHeaderFields?["Cookie"] {
|
||||
let escapedValue = cookie.replacingOccurrences(of: "\"", with: "\\\"")
|
||||
components.append("--cookie \"\(escapedValue)\"")
|
||||
}
|
||||
|
||||
// URL
|
||||
components.append("\"\(request.url!.absoluteString)\"")
|
||||
|
||||
return components.joined(separator: " \\\n\t")
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
#
|
||||
# .swiftformat
|
||||
# Created by Felix Mau (https://felix.hamburg)
|
||||
#
|
||||
|
||||
#
|
||||
# Configuration file for SwiftFormat (https://github.com/nicklockwood/SwiftFormat/)
|
||||
#
|
||||
# A more detailed documentation of the rules can be found at
|
||||
# https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md
|
||||
#
|
||||
|
||||
#
|
||||
# File options
|
||||
#
|
||||
# Note: Excluded paths are relative to the SwiftFormat configuration file.
|
||||
# Therefore we can't specify them in the here. Just leaving this commented out line here as a reminder.
|
||||
#
|
||||
# --exclude Pods,Generated
|
||||
|
||||
#
|
||||
# Rules
|
||||
#
|
||||
--disable andOperator
|
||||
--disable blankLinesAtStartOfScope
|
||||
--disable wrapMultilineStatementBraces
|
||||
|
||||
#
|
||||
# Rule Configuration
|
||||
#
|
||||
--wraparguments "after-first"
|
||||
--wrapcollections "before-first"
|
||||
--importgrouping testable-last
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
#
|
||||
# .swiftlint.yml
|
||||
#
|
||||
# This file enables [SwiftLint](https://github.com/realm/SwiftLint) rules, that are disabled by default.
|
||||
#
|
||||
# Created by Felix Mau (https://felix.hamburg)
|
||||
#
|
||||
# Based on:
|
||||
# - https://gist.github.com/tweetjay/1b7f00b6f312a5a2cf7b5676420b9b0c
|
||||
# - https://github.com/brandenr/swiftlintconfig
|
||||
# - https://www.avanderlee.com/optimization/swiftlint-optin-rules/
|
||||
#
|
||||
|
||||
opt_in_rules:
|
||||
- anyobject_protocol # Prefer using `AnyObject` over class for class-only protocols.
|
||||
- array_init # Prefer using `Array(seq)` over `seq.map { $0 }` to convert a sequence into an Array.
|
||||
- balanced_xctest_lifecycle # Test classes must implement balanced setUp and tearDown methods.
|
||||
- block_based_kvo # Prefer the new block based KVO API with keypaths when using Swift 3.2 or later.
|
||||
- closure_spacing # Closure expressions should have a single space inside each brace.
|
||||
- contains_over_filter_count # Prefer `contains` over comparing `filter(where:).count` to 0.
|
||||
- contains_over_filter_is_empty # Prefer `contains` over using `filter(where:).isEmpty`.
|
||||
- contains_over_first_not_nil # Prefer `contains` over `first(where:) != nil` and `firstIndex(where:) != nil`.
|
||||
- convenience_type # Types used for hosting only static members should be implemented as a caseless enum to avoid instantiation.
|
||||
- empty_collection_literal # Prefer checking `isEmpty` over comparing collection to an empty array or dictionary literal.
|
||||
- empty_count # Prefer checking `isEmpty` over comparing count to zero.
|
||||
- empty_parameters # Prefer `() ->` over `Void ->`.
|
||||
- empty_string # Prefer checking `isEmpty` over comparing string to an empty string literal.
|
||||
- empty_xctest_method # Empty XCTest method should be avoided.
|
||||
- explicit_init # Explicitly calling .init() should be avoided.
|
||||
- fallthrough # Fallthrough should be avoided.
|
||||
- fatal_error_message # A fatalError call should have a message.
|
||||
- file_header # Header comments should be consistent with project patterns.
|
||||
- first_where # Prefer using `.first(where:)` over `.filter { }.first` in collections.
|
||||
- for_where # `where` clauses are preferred over a single `if` inside a `for`.
|
||||
- force_unwrapping # Force unwrapping should be avoided.
|
||||
- identical_operands # Comparing two identical operands is likely a mistake.
|
||||
- is_disjoint # Prefer using `Set.isDisjoint(with:)` over `Set.intersection(_:).isEmpty`.
|
||||
- last_where # Prefer using `.last(where:)` over `.filter { }.last` in collections.
|
||||
- legacy_multiple # Prefer using the `isMultiple(of:)` function instead of using the remainder operator (`%`).
|
||||
- legacy_random # Prefer using `type.random(in:)` over legacy functions, e.g. `arc4random()`.
|
||||
- lower_acl_than_parent # Ensure definitions have a lower access control level than their enclosing parent.
|
||||
- missing_docs # Declarations should be documented.
|
||||
- modifier_order # Modifier order should be consistent.
|
||||
- object_literal # Prefer object literals over image and color inits.
|
||||
- operator_usage_whitespace # Operators should be surrounded by a single whitespace when they are being used.
|
||||
- optional_enum_case_matching # Matching an enum case against an optional enum without ‘?’.
|
||||
- overridden_super_call # Some overridden methods should always call super
|
||||
- prefer_self_type_over_type_of_self # Prefer `Self` over `type(of: self)` when accessing properties or calling methods.
|
||||
- prefer_zero_over_explicit_init # Prefer `.zero` over explicit init with zero parameters (e.g. `CGPoint(x: 0, y: 0)`).
|
||||
- private_subject # Combine Subject should be private.
|
||||
- private_outlet # IBOutlets should be private to avoid leaking UIKit to higher layers.
|
||||
- private_action # IBActions should be private.
|
||||
- prohibited_super_call # Some methods should not call super, e.g. `UIViewController.loadView()`
|
||||
- sorted_first_last # Prefer using `min()` or `max()` over `sorted().first` or `sorted().last`
|
||||
- toggle_bool # Prefer `someBool.toggle()` over `someBool = !someBool`.
|
||||
- unavailable_function # Unimplemented functions should be marked as unavailable.
|
||||
- unused_import # All imported modules should be required to make the file compile.
|
||||
- unused_optional_binding # Prefer `!= nil` over `let _ =`
|
||||
- unused_declaration # Declarations should be referenced at least once within all files linted.
|
||||
- void_return # Prefer `-> Void` over `-> ()`.
|
||||
|
||||
|
||||
file_header:
|
||||
required_pattern: |
|
||||
\/\/
|
||||
\/\/ SWIFTLINT_CURRENT_FILENAME
|
||||
|
||||
line_length: 160
|
||||
|
||||
trailing_comma:
|
||||
mandatory_comma: true
|
||||
|
||||
custom_rules:
|
||||
comments_space:
|
||||
name: 'Space After Comment'
|
||||
regex: '(^ *//\w+)'
|
||||
message: 'There should be a space after //.'
|
||||
severity: warning
|
||||
empty_line_after_guard:
|
||||
name: 'Empty Line After Guard'
|
||||
regex: '(^ *guard[ a-zA-Z0-9=?.\(\),><!]*\{[ a-zA-Z0-9=?.\(\),><!]*\}\n *(?!(?:return|guard))\S+)'
|
||||
message: 'There should be an empty line after a guard.'
|
||||
severity: warning
|
||||
empty_line_after_super:
|
||||
name: 'Empty Line After Super'
|
||||
regex: '(^ *super\.[ a-zA-Z0-9=?.\(\)\{\}:,><!]*\n *(?!(?:\}|return))\S+)'
|
||||
message: 'There should be an empty line after super.'
|
||||
severity: warning
|
||||
unnecessary_type:
|
||||
name: 'Unnecessary Type'
|
||||
regex: '[ a-zA-Z0-9]*(?:let|var) [ a-zA-Z0-9]*: ([a-zA-Z0-9]*) *= \1'
|
||||
message: 'Type Definition Not Needed.'
|
||||
severity: warning
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2020 Felix Mau <me@felix.hamburg>
|
||||
|
||||
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.
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
# Swift Configuration Files 🛠
|
||||
Repository containing configuration files for Swift development, e.g. for tools like [SwiftLint](https://github.com/realm/SwiftLint) or [SwiftFormat](https://github.com/nicklockwood/SwiftFormat).
|
||||
|
||||
### Author
|
||||
Felix Mau (me(@)felix.hamburg)
|
||||
|
||||
### License
|
||||
|
||||
Swift Configuration Files is available under the MIT license. See the LICENSE file for more info.
|
||||
Binary file not shown.
Generated
-21
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Nick Lockwood
|
||||
|
||||
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.
|
||||
Generated
-886
@@ -1,886 +0,0 @@
|
||||

|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9ZGWNK5FEZFF6&source=url)
|
||||
[](https://github.com/nicklockwood/SwiftFormat/actions/workflows/build.yml)
|
||||
[](https://codecov.io/gh/nicklockwood/SwiftFormat)
|
||||
[](https://developer.apple.com/swift)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](http://twitter.com/nicklockwood)
|
||||
|
||||
Table of Contents
|
||||
-----------------
|
||||
|
||||
- [What?](#what-is-this)
|
||||
- [Why?](#why-would-i-want-to-do-that)
|
||||
- [How?](#how-do-i-install-it)
|
||||
- [Command-line tool](#command-line-tool)
|
||||
- [Xcode source editor extension](#xcode-source-editor-extension)
|
||||
- [Xcode build phase](#xcode-build-phase)
|
||||
- [Via Applescript](#via-applescript)
|
||||
- [VSCode plugin](#vscode-plugin)
|
||||
- [Sublime Text plugin](#sublime-text-plugin)
|
||||
- [Git pre-commit hook](#git-pre-commit-hook)
|
||||
- [On CI using Danger](#on-ci-using-danger)
|
||||
- [Bazel build](#bazel-build)
|
||||
- [Docker](#docker)
|
||||
- [Configuration](#configuration)
|
||||
- [Options](#options)
|
||||
- [Rules](#rules)
|
||||
- [Swift version](#swift-version)
|
||||
- [Config file](#config-file)
|
||||
- [Globs](#globs)
|
||||
- [Linting](#linting)
|
||||
- [Error codes](#error-codes)
|
||||
- [Cache](#cache)
|
||||
- [File headers](#file-headers)
|
||||
- [FAQ](#faq)
|
||||
- [Known issues](#known-issues)
|
||||
- [Tip Jar](#tip-jar)
|
||||
- [Credits](#credits)
|
||||
|
||||
|
||||
What is this?
|
||||
----------------
|
||||
|
||||
SwiftFormat is a code library and command-line tool for reformatting Swift code on macOS or Linux.
|
||||
|
||||
SwiftFormat goes above and beyond what you might expect from a code formatter. In addition to adjusting white space it can insert or remove implicit `self`, remove redundant parentheses, and correct many other deviations from the standard Swift idioms.
|
||||
|
||||
|
||||
Why would I want to do that?
|
||||
-----------------------------
|
||||
|
||||
Many programmers have a preferred style for formatting their code, and others seem entirely blind to the existing formatting conventions of a project (to the enragement of their colleagues).
|
||||
|
||||
When collaborating on a project, it can be helpful to agree on a common coding style, but enforcing that manually is tedious and error-prone, and can lead to arguments if some participants take it more seriously than others.
|
||||
|
||||
Having a tool to automatically enforce a common style eliminates those issues, and lets you focus on the behavior of the code, not its presentation.
|
||||
|
||||
|
||||
How do I install it?
|
||||
---------------------
|
||||
|
||||
That depends - There are several ways you can use SwiftFormat:
|
||||
|
||||
1. As a command-line tool that you run manually, or as part of some other toolchain
|
||||
2. As a Source Editor Extension that you can invoke via the Editor > SwiftFormat menu within Xcode
|
||||
3. As a build phase in your Xcode project, so that it runs every time you press Cmd-R or Cmd-B, or
|
||||
4. As a Git pre-commit hook, so that it runs on any files you've changed before you check them in
|
||||
|
||||
|
||||
Command-line tool
|
||||
-------------------
|
||||
|
||||
**NOTE:** if you are using any of the following methods to install SwiftFormat on macOS 10.14.3 or earlier and are experiencing a crash on launch, you may need to install the [Swift 5 Runtime Support for Command Line Tools](https://support.apple.com/kb/DL1998). See [known issues](#known-issues) for details.
|
||||
|
||||
**Installation:**
|
||||
|
||||
You can install the `swiftformat` command-line tool on macOS or Linux using [Homebrew](http://brew.sh/). Assuming you already have Homebrew installed, just type:
|
||||
|
||||
```bash
|
||||
$ brew install swiftformat
|
||||
```
|
||||
|
||||
To update to the latest version once installed:
|
||||
|
||||
```bash
|
||||
$ brew upgrade swiftformat
|
||||
```
|
||||
|
||||
Alternatively, you can install the tool on macOS or Linux by using [Mint](https://github.com/yonaskolb/Mint) as follows:
|
||||
|
||||
```bash
|
||||
$ mint install nicklockwood/SwiftFormat
|
||||
```
|
||||
|
||||
Or if you prefer, you can check out and build SwiftFormat manually on macOS, Linux or Windows as follows:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/nicklockwood/SwiftFormat
|
||||
$ cd SwiftFormat
|
||||
$ swift build -c release
|
||||
```
|
||||
|
||||
If you are installing SwiftFormat into your project directory, you can use [CocoaPods](https://cocoapods.org/) on macOS to automatically install the swiftformat binary along with your other pods - see the Xcode build phase instructions below for details.
|
||||
|
||||
Another option is to include the binary artifactbundle in your `Package.swift`:
|
||||
|
||||
```swift
|
||||
.binaryTarget(
|
||||
name: "SwiftFormat",
|
||||
url: "https://github.com/nicklockwood/SwiftFormat/releases/download/0.49.12/swiftformat-macos.artifactbundle.zip",
|
||||
checksum: "CHECKSUM"
|
||||
),
|
||||
```
|
||||
|
||||
If you would prefer not to use a package manager, you can build the command-line app manually:
|
||||
|
||||
1. open `SwiftFormat.xcodeproj` and build the `SwiftFormat (Application)` scheme.
|
||||
|
||||
2. Drag the `swiftformat` binary into `/usr/local/bin/` (this is a hidden folder, but you can use the Finder's `Go > Go to Folder...` menu to open it).
|
||||
|
||||
3. Open `~/.bash_profile` in your favorite text editor (this is a hidden file, but you can type `open ~/.bash_profile` in the terminal to open it).
|
||||
|
||||
4. Add the following line to the file: `alias swiftformat="/usr/local/bin/swiftformat --indent 4"` (you can omit the `--indent 4`, or replace it with something else. Run `swiftformat --help` to see the available options).
|
||||
|
||||
5. Save the `.bash_profile` file and run the command `source ~/.bash_profile` for the changes to take effect.
|
||||
|
||||
**Usage:**
|
||||
|
||||
If you followed the installation instructions above, you can now just type
|
||||
|
||||
```bash
|
||||
$ swiftformat .
|
||||
```
|
||||
|
||||
(that's a space and then a period after the command) in the terminal to format any Swift files in the current directory. In place of the `.`, you can instead type an absolute or relative path to the file or directory that you want to format.
|
||||
|
||||
**WARNING:** `swiftformat .` will overwrite any Swift files it finds in the current directory, and any subfolders therein. If you run it in your home directory, it will probably reformat every Swift file on your hard drive.
|
||||
|
||||
To use it safely, do the following:
|
||||
|
||||
1. Choose a file or directory that you want to apply the changes to.
|
||||
|
||||
2. Make sure that you have committed all your changes to that code safely in git (or whatever source control system you use).
|
||||
|
||||
3. (Optional) In Terminal, type `swiftformat --inferoptions "/path/to/your/code/"`. This will suggest a set of formatting options to use that match your existing project style (but you are free to ignore these and use the defaults, or your own settings if you prefer).
|
||||
|
||||
The path can point to either a single Swift file or a directory of files. It can be either be absolute, or relative to the current directory. The `""` quotes around the path are optional, but if the path contains spaces then you either need to use quotes, or escape each space with `\`. You may include multiple paths separated by spaces.
|
||||
|
||||
4. In Terminal, type `swiftformat "/path/to/your/code/"`. The same rules apply as above with respect to paths, and multiple space-delimited paths are allowed.
|
||||
|
||||
If you used `--inferoptions` to generate a suggested set of options in step 3, you should copy and paste them into the command, either before or after the path(s) to your source files.
|
||||
|
||||
If you have created a [config file](#config-file), you can specify its path using `--config "/path/to/your/config-file/"`. Alternatively, if you name the file `.swiftformat` and place it inside the project you are formatting, it will be picked up automatically.
|
||||
|
||||
5. Press enter to begin formatting. Once the formatting is complete, use your source control system to check the changes, and verify that no undesirable changes have been introduced. If they have, revert the changes, tweak the options and try again.
|
||||
|
||||
6. (Optional) commit the changes.
|
||||
|
||||
Following these instructions *should* ensure that you avoid catastrophic data loss, but in the unlikely event that it wipes your hard drive, **please note that I accept no responsibility**.
|
||||
|
||||
**Using Standard Input/Output:**
|
||||
|
||||
If you prefer, you can use unix pipes to include SwiftFormat as part of a command chain. For example, this is an alternative way to format a file:
|
||||
|
||||
```bash
|
||||
$ cat /path/to/file.swift | swiftformat --output /path/to/file.swift
|
||||
```
|
||||
|
||||
Omitting the `--output /path/to/file.swift` will print the formatted file to Standard Output (stdout). You can also pass "stdout" explicitly as the output path:
|
||||
|
||||
```bash
|
||||
$ cat /path/to/file.swift | swiftformat --output stdout
|
||||
```
|
||||
|
||||
Or you can use `>` to specify the output path as follows:
|
||||
|
||||
```bash
|
||||
$ cat /path/to/file.swift | swiftformat > /path/to/file.swift
|
||||
```
|
||||
|
||||
If you do not supply an input file, SwiftFormat will automatically take its input from Standard Input (stdin), but will time-out if no input is received immediately and display the help screen. To make it explicit, pass "stdin" as the input path:
|
||||
|
||||
```bash
|
||||
$ cat /path/to/file.swift | swiftformat stdin
|
||||
```
|
||||
|
||||
When using stdin, SwiftFormat does not have access to the file path of the input, so features that rely on the file location (such as inserting the creation date into header comments, or detecting `.swiftformat` configuration files in the file path) will not work. To solve this, you can provide the file path using the `--stdinpath` argument:
|
||||
|
||||
```bash
|
||||
$ cat /path/to/file.swift | swiftformat stdin --stdinpath /path/to/file.swift
|
||||
```
|
||||
|
||||
|
||||
Xcode source editor extension
|
||||
-----------------------------
|
||||
|
||||
**Installation:**
|
||||
|
||||
Like the command-line tool, you can install the SwiftFormat for Xcode extension application via [Homebrew](http://brew.sh/). Assuming you already have Homebrew installed, type:
|
||||
|
||||
```bash
|
||||
$ brew install --cask swiftformat-for-xcode
|
||||
```
|
||||
|
||||
This will install SwiftFormat for Xcode in your Applications folder. Double-click the app to launch it, and then follow the on-screen instructions.
|
||||
|
||||
**NOTE:** The app should be correctly signed, but if you get a Gatekeeper warning when trying to open it you can bypass this by right-clicking (or control-clicking) the app and selecting `Open`.
|
||||
|
||||
To update to the latest version once installed use:
|
||||
|
||||
```bash
|
||||
$ brew upgrade --cask swiftformat-for-xcode
|
||||
```
|
||||
|
||||
Alternatively, if you prefer not to use Homebrew, you'll find the latest version of the SwiftFormat for Xcode application inside the EditorExtension folder included in the SwiftFormat repository. Download and unpack the zip archive, then drag `SwiftFormat for Xcode.app` into your `Applications` folder.
|
||||
|
||||
**Usage:**
|
||||
|
||||
Once you have launched the app and restarted Xcode, you'll find a SwiftFormat option under Xcode's Editor menu. If the SwiftFormat menu does not appear [this thread](https://github.com/nicklockwood/SwiftFormat/issues/494) may help.
|
||||
|
||||
You can configure the formatting [rules](#rules) and [options](#options) using the SwiftFormat for Xcode host application. There is currently no way to override these per-project, however, you can import and export different configurations using the File menu. You will need to do this again each time you switch projects.
|
||||
|
||||
The format of the configuration file is described in the [Config section](#config-file) below.
|
||||
|
||||
**Note:** SwiftFormat for Xcode cannot automatically detect changes to an imported configuration file. If you update the `.swiftformat` file for your project, you will need to manually re-import that file into SwiftFormat for Xcode in order for the Xcode source editor extension to use the new configuration.
|
||||
|
||||
|
||||
Xcode build phase
|
||||
-------------------
|
||||
|
||||
**NOTE:** Adding this script will overwrite your source files as you work on them, which has the annoying side-effect of clearing the undo history. You may wish to add the script to your test target rather than your main target, so that it is invoked only when you run the unit tests, and not every time you build the app.
|
||||
|
||||
Alternatively, you might want to consider running SwiftFormat in [lint](#linting) mode as part of your normal build, and then running a formatting pass manually, or as part of a less-frequent build target (such as the tests).
|
||||
|
||||
### Using Swift Package Manager
|
||||
|
||||
To set up SwiftFormat as an Xcode build phase, do the following:
|
||||
|
||||
#### 1) Create a BuildTools folder and Package.swift
|
||||
|
||||
1. Create a folder called `BuildTools` in the same folder as your xcodeproj file
|
||||
2. In this folder, create a file called `Package.swift`, with the following contents:
|
||||
```swift
|
||||
// swift-tools-version:5.1
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "BuildTools",
|
||||
platforms: [.macOS(.v10_11)],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.49.0"),
|
||||
],
|
||||
targets: [.target(name: "BuildTools", path: "")]
|
||||
)
|
||||
```
|
||||
3. If you are running Xcode 11.4 or later, in the `BuildTools` folder create a file called `Empty.swift` with nothing in it. This is to satisfy a change in Swift Package Manager.
|
||||
|
||||
#### 2) Add a Build phase to your app target
|
||||
|
||||
1. Click on your project in the file list, choose your target under `TARGETS`, click the `Build Phases` tab
|
||||
2. Add a `New Run Script Phase` by clicking the little plus icon in the top left
|
||||
3. Drag the new `Run Script` phase **above** the `Compile Sources` phase, expand it and paste the following script:
|
||||
|
||||
```bash
|
||||
cd BuildTools
|
||||
SDKROOT=(xcrun --sdk macosx --show-sdk-path)
|
||||
#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file
|
||||
swift run -c release swiftformat "$SRCROOT"
|
||||
```
|
||||
|
||||
You can also use `swift run -c release --package-path BuildTools swiftformat "$SRCROOT"` if you need a more complex script and `cd BuildTools` breaks stuff.
|
||||
|
||||
**NOTE:** You may wish to check BuildTools/Package.swift into your source control so that the version used by your run-script phase is kept in version control. It is recommended to add the following to your .gitignore file: `BuildTools/.build` and `BuildTools/.swiftpm`.
|
||||
|
||||
### Using CocoaPods
|
||||
|
||||
#### 1) Add the SwiftFormat CLI to your Podfile
|
||||
|
||||
1. Add the `swiftformat` binary to your project directory via [CocoaPods](https://cocoapods.org/), by adding the following line to your Podfile then running `pod install`:
|
||||
|
||||
```ruby
|
||||
pod 'SwiftFormat/CLI', '~> 0.49'
|
||||
```
|
||||
|
||||
**NOTE:** This will only install the pre-built command-line app, not the source code for the SwiftFormat framework.
|
||||
|
||||
**NOTE (2):** When installing this way, GateKeeper may block swiftformat from running until you open it manually the first time by right-clicking in the Finder and selecting "Open".
|
||||
|
||||
#### 2) Add a Build phase to your app target
|
||||
|
||||
1. Click on your project in the file list, choose your target under `TARGETS`, click the `Build Phases` tab
|
||||
2. Add a `New Run Script Phase` by clicking the little plus icon in the top left
|
||||
3. Drag the new `Run Script` phase **above** the `Compile Sources` phase, expand it and paste the following script:
|
||||
|
||||
```bash
|
||||
"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat" "$SRCROOT"
|
||||
```
|
||||
|
||||
### Alternative: Locally installed SwiftFormat
|
||||
|
||||
Alternatively, you could use a locally installed swiftformat command-line tool instead by putting the following in your Run Script build phase:
|
||||
|
||||
```bash
|
||||
if which swiftformat >/dev/null; then
|
||||
swiftformat .
|
||||
else
|
||||
echo "warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat"
|
||||
fi
|
||||
```
|
||||
|
||||
This is not recommended for shared projects however, as different team members using different versions of SwiftFormat may result in noise in the commit history as code gets reformatted inconsistently.
|
||||
|
||||
|
||||
Via AppleScript
|
||||
----------------
|
||||
|
||||
To run SwiftFormat on the frontmost Xcode document (project or workspace) you can use the following AppleScript:
|
||||
|
||||
```applescript
|
||||
tell application "Xcode"
|
||||
set frontWindow to the first window
|
||||
set myPath to path of document of frontWindow
|
||||
do shell script "cd " & myPath & ";cd ..; /usr/local/bin/swiftformat ."
|
||||
end tell
|
||||
```
|
||||
|
||||
Some Apps you can trigger this from are [BetterTouchTool](https://folivora.ai), [Alfred](https://www.alfredapp.com) or [Keyboard Maestro](https://www.keyboardmaestro.com/main/). Another option is to define a QuickAction for Xcode via Automator and then assign a keyboard shortcut for it in the System Preferences.
|
||||
|
||||
|
||||
VSCode plugin
|
||||
--------------
|
||||
|
||||
If you prefer to use Microsoft's [VSCode](https://code.visualstudio.com) editor for writing Swift, [Valentin Knabel](https://github.com/vknabel) has created a [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swiftformat) for SwiftFormat.
|
||||
|
||||
|
||||
Sublime Text plugin
|
||||
--------------------
|
||||
|
||||
If you prefer to use the [Sublime Text](https://www.sublimetext.com) editor, try the [Sublime-Swift-Format plugin](https://github.com/aerobounce/Sublime-Swift-Format) by [Aerobounce](https://github.com/aerobounce).
|
||||
|
||||
|
||||
Git pre-commit hook
|
||||
---------------------
|
||||
|
||||
1. Follow the instructions for installing the SwiftFormat command-line tool.
|
||||
|
||||
2. Install [git-format-staged](https://github.com/hallettj/git-format-staged).
|
||||
|
||||
3. Edit or create a `.git/hooks/pre-commit` file in your project folder. The .git folder is hidden but should already exist if you are using Git with your project, so open it with the terminal, or the Finder's `Go > Go to Folder...` menu.
|
||||
|
||||
4. Add the following line in the pre-commit file. The `{}` will be replaced automatically by the path to the Swift file being formatted:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
git-format-staged --formatter "swiftformat stdin --stdinpath '{}'" "*.swift"
|
||||
```
|
||||
|
||||
(Note that this example uses your locally installed version of SwiftFormat, not a separate copy in your project repository. You can replace `swiftformat` with the path to a copy inside your project if you prefer.)
|
||||
|
||||
5. enable the hook by typing `chmod +x .git/hooks/pre-commit` in the terminal.
|
||||
|
||||
The pre-commit hook will now run whenever you run `git commit`. Running `git commit --no-verify` will skip the pre-commit hook.
|
||||
|
||||
**NOTE:** If you are using Git via a GUI client such as [Tower](https://www.git-tower.com), [additional steps](https://www.git-tower.com/help/mac/faq-and-tips/faq/hook-scripts) may be needed.
|
||||
|
||||
**NOTE (2):** Unlike the Xcode build phase approach, git pre-commit hook won't be checked in to source control, and there's no way to guarantee that all users of the project are using the same version of SwiftFormat. For a collaborative project, you might want to consider a *post*-commit hook instead, which would run on your continuous integration server.
|
||||
|
||||
On CI using Danger
|
||||
-------------------
|
||||
|
||||
To setup SwiftFormat to be used by your continuous integration system using [Danger](http://danger.systems/ruby/), do the following:
|
||||
|
||||
1. Follow the [`instructions`](http://danger.systems/guides/getting_started.html) to setup Danger.
|
||||
2. Add the [`danger-swiftformat`](https://github.com/garriguv/danger-ruby-swiftformat) plugin to your `Gemfile`.
|
||||
3. Add the following to your `Dangerfile`:
|
||||
|
||||
```ruby
|
||||
swiftformat.binary_path = "/path/to/swiftformat" # optional
|
||||
swiftformat.additional_args = "--indent tab --self insert" # optional
|
||||
swiftformat.check_format(fail_on_error: true)
|
||||
```
|
||||
|
||||
**NOTE:** It is recommended to add the `swiftformat` binary to your project directory to ensure the same version is used each time (see the [Xcode build phase](#xcode-build-phase) instructions above).
|
||||
|
||||
|
||||
Bazel Build
|
||||
-----------
|
||||
|
||||
If you use [Bazel](https://bazel.build/) to build your Swift projects and want to ensure that only properly formatted code is merged to your main branch, try [rules_swiftformat](https://github.com/cgrindel/rules_swiftformat). The repository contains Bazel rules and macros that format Swift source files using SwiftFormat, test that the formatted files exist in the workspace directory, and copy the formatted files to the workspace directory.
|
||||
|
||||
|
||||
Docker
|
||||
-----------
|
||||
|
||||
SwiftFormat publishes releases into [GitHub Packages](https://github.com/features/packages) Docker registry. To pull the image call:
|
||||
|
||||
```bash
|
||||
$ docker pull ghcr.io/nicklockwood/swiftformat:latest
|
||||
```
|
||||
|
||||
By default, the container runs `swiftformat .` Therefore, you need to provide a path either via an argument:
|
||||
|
||||
```bash
|
||||
docker run --rm -v /local/source/path:/work ghcr.io/nicklockwood/swiftformat:latest /work
|
||||
```
|
||||
|
||||
or by changing the working dir:
|
||||
|
||||
```bash
|
||||
docker run --rm -v /local/source/path:/work -w /work ghcr.io/nicklockwood/swiftformat:latest
|
||||
```
|
||||
|
||||
To check the installed SwiftFormat version:
|
||||
|
||||
```bash
|
||||
docker run --rm ghcr.io/nicklockwood/swiftformat:latest --version
|
||||
```
|
||||
|
||||
Linting example:
|
||||
|
||||
```bash
|
||||
docker run --rm -v /local/source/path:/work ghcr.io/nicklockwood/swiftformat:latest /work --lint
|
||||
```
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
SwiftFormat's configuration is split between **rules** and **options**. Rules are functions in the SwiftFormat library that apply changes to the code. Options are settings that control the behavior of the rules.
|
||||
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
The options available in SwiftFormat can be displayed using the `--options` command-line argument. The default value for each option is indicated in the help text.
|
||||
|
||||
Rules are configured by adding `--[option_name] [value]` to your command-line arguments, or by creating a `.swiftformat` [config file](#config-file) and placing it in your project directory.
|
||||
|
||||
A given option may affect multiple rules. Use `--ruleinfo [rule_name]` command for details about which options affect a given rule, or see the [Rules.md](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md) file.
|
||||
|
||||
You can configure options for specific files or code ranges by using `swiftformat:options` directive in comments inside your Swift file. To temporarily set one or more options inside a source file, use:
|
||||
|
||||
```swift
|
||||
// swiftformat:options --indent 2 --allman true
|
||||
```
|
||||
|
||||
To apply an options override only to a particular line, use the `:next` modifier:
|
||||
|
||||
```swift
|
||||
// swiftformat:options:next --semicolons inline
|
||||
doTheThing(); print("Did the thing")
|
||||
```
|
||||
|
||||
|
||||
Rules
|
||||
-----
|
||||
|
||||
SwiftFormat includes over 50 rules, and new ones are added all the time. An up-to-date list can be found in [Rules.md](https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md) along with documentation for how they are used.
|
||||
|
||||
The list of available rules can be displayed within the command-line app using the `--rules` argument. Rules can be either enabled or disabled. Most are enabled by default. Disabled rules are marked with "(disabled)" when displayed using `--rules`.
|
||||
|
||||
You can use the `--ruleinfo [rule_name]` command to get information about a specific rule. Pass a comma-delimited list of rule names to get information for multiple rules at once, or use `--ruleinfo` with no argument for info on all rules.
|
||||
|
||||
You can disable rules individually using `--disable` followed by a list of one or more comma-delimited rule names, or enable opt-in rules using `--enable` followed by the rule names:
|
||||
|
||||
```bash
|
||||
--disable redundantSelf,trailingClosures
|
||||
--enable isEmpty
|
||||
```
|
||||
|
||||
If you prefer, you can use multiple `--enable`/`--disable` arguments instead of using commas:
|
||||
|
||||
```bash
|
||||
--disable indent
|
||||
--disable linebreaks
|
||||
--disable redundantSelf
|
||||
```
|
||||
|
||||
Alternatively, you can use the line continuation character `\` to wrap a single argument over multiple line:
|
||||
|
||||
```bash
|
||||
--disable \
|
||||
indent, \
|
||||
linebreaks, \
|
||||
redundantSelf
|
||||
```
|
||||
|
||||
To avoid automatically opting-in to new rules when SwiftFormat is updated, you can disable all rules using:
|
||||
|
||||
```bash
|
||||
--disable all
|
||||
```
|
||||
|
||||
And then individually enable just the rules you want. Alternatively, use the`--rules` argument to *only* enable the rules you specify:
|
||||
|
||||
```bash
|
||||
--rules indent,linebreaks
|
||||
```
|
||||
|
||||
As above, you may include multiple `--rules` arguments, or use the line continuation character `\` to wrap the rules onto separate lines:
|
||||
|
||||
```bash
|
||||
--rules redundantSelf
|
||||
--rules \
|
||||
indent, \
|
||||
linebreaks
|
||||
```
|
||||
|
||||
To see exactly which rules were applied to a given file, you can use the `--verbose` command-line option to force SwiftFormat to print a more detailed log as it applies the formatting. **NOTE:** running in verbose mode is slower than the default mode.
|
||||
|
||||
You can disable rules for specific files or code ranges by using `swiftformat:` directives in comments inside your Swift file. To temporarily disable one or more rules inside a source file, use:
|
||||
|
||||
```swift
|
||||
// swiftformat:disable <rule1> [<rule2> [rule<3> ...]]
|
||||
```
|
||||
|
||||
To enable the rule(s) again, use:
|
||||
|
||||
```swift
|
||||
// swiftformat:enable <rule1> [<rule2> [rule<3> ...]]
|
||||
```
|
||||
|
||||
To disable all rules use:
|
||||
|
||||
```swift
|
||||
// swiftformat:disable all
|
||||
```
|
||||
|
||||
And to enable them all again, use:
|
||||
|
||||
```swift
|
||||
// swiftformat:enable all
|
||||
```
|
||||
|
||||
To temporarily prevent one or more rules being applied to just the next line, use:
|
||||
|
||||
```swift
|
||||
// swiftformat:disable:next <rule1> [<rule2> [rule<3> ...]]
|
||||
let foo = bar // rule(s) will be disabled for this line
|
||||
let bar = baz // rule(s) will be re-enabled for this line
|
||||
```
|
||||
|
||||
There is no need to manually re-enable a rule after using the `next` directive.
|
||||
|
||||
**NOTE:** The `swiftformat:enable` directives only serves to counter a previous `swiftformat:disable` directive in the same file. It is not possible to use `swiftformat:enable` to enable a rule that was not already enabled when formatting started.
|
||||
|
||||
|
||||
Swift version
|
||||
-------------
|
||||
|
||||
Most SwiftFormat rules are version-agnostic, but some are applicable only to newer Swift versions. These rules will be disabled automatically if the Swift version is not specified, so to make sure that the full functionality is available you should specify the version of Swift that is used by your project.
|
||||
|
||||
You can specify the Swift version in one of two ways:
|
||||
|
||||
The preferred option is to add a `.swift-version` file to your project directory. This is a text file that should contain the minimum Swift version supported by your project, and is a standard already used by other tools.
|
||||
|
||||
The `.swift-version` file applies hierarchically; If you have submodules in your project that use a different Swift version, you can add separate `.swift-version` files to those directories.
|
||||
|
||||
The other option to specify the Swift version using the `--swiftversion` command line argument. Note that this will be overridden by any `.swift-version` files encountered while processing.
|
||||
|
||||
|
||||
Config file
|
||||
-----------
|
||||
|
||||
Although it is possible to configure SwiftFormat directly by using the command-line [options](#options) and [rules](#rules) detailed above, it is sometimes more convenient to create a configuration file, which can be added to your project and shared with other developers.
|
||||
|
||||
A SwiftFormat configuration file consists of one or more command-line options, split onto separate lines, e.g:
|
||||
|
||||
```
|
||||
--allman true
|
||||
--indent tab
|
||||
--disable elseOnSameLine,semicolons
|
||||
```
|
||||
|
||||
While formatting, SwiftFormat will automatically check inside each subdirectory for the presence of a `.swiftformat` file and will apply any options that it finds there to the files in that directory.
|
||||
|
||||
This allows you to override certain rules or formatting options just for a particular directory of files. You can also specify excluded files relative to that directory using `--exclude`, which may be more convenient than specifying them at the top-level:
|
||||
|
||||
```
|
||||
--exclude Pods,Generated
|
||||
```
|
||||
|
||||
The `--exclude` option takes a comma-delimited list of file or directory paths to exclude from formatting. Excluded paths are relative to the config file containing the `--exclude` command. The excluded paths can include wildcards, specified using Unix "Glob" syntax, as [documented below](#globs).
|
||||
|
||||
Config files named ".swiftformat" will be processed automatically, however, you can select an additional configuration file to use for formatting using the `--config "path/to/config/file"` command-line argument. A configuration file selected using `--config` does not need to be named ".swiftformat", and can be located outside of the project directory.
|
||||
|
||||
The config file format is designed to be edited by hand. You may include blank lines for readability, and can also add comments using a hash prefix (#), e.g.
|
||||
|
||||
```
|
||||
# format options
|
||||
--allman true
|
||||
--indent tab # tabs FTW!
|
||||
|
||||
# file options
|
||||
--exclude Pods
|
||||
|
||||
# rules
|
||||
--disable elseOnSameLine,semicolons
|
||||
```
|
||||
|
||||
If you would prefer not to edit the configuration file by hand, you can use the [SwiftFormat for Xcode](#xcode-source-editor-extension) app to edit the configuration and export a configuration file. You can also use the swiftformat command-line-tool's `--inferoptions` command to generate a config file from your existing project, like this:
|
||||
|
||||
```bash
|
||||
$ cd /path/to/project
|
||||
$ swiftformat --inferoptions . --output .swiftformat
|
||||
```
|
||||
|
||||
Globs
|
||||
-----
|
||||
|
||||
When excluding files from formatting using the `--exclude` option, you may wish to make use of wildcard paths (aka "Globs") to match all files that match a particular naming convention without having to manually list them all.
|
||||
|
||||
SwiftFormat's glob syntax is based on Ruby's implementation, which varies slightly from the Unix standard. The following patterns are supported:
|
||||
|
||||
* `*` - A single star matches zero or more characters in a filename, but *not* a `/`.
|
||||
|
||||
* `**` - A double star will match anything, including one or more `/`.
|
||||
|
||||
* `?` - A question mark will match any single character except `/`.
|
||||
|
||||
* `[abc]` - Matches any single character inside the brackets.
|
||||
|
||||
* `[a-z]` - Matches a single character in the specified range in the brackets.
|
||||
|
||||
* `{foo,bar}` - Matches any one of the comma-delimited strings inside the braces.
|
||||
|
||||
Examples:
|
||||
|
||||
* `foo.swift` - Matches the file "foo.swift" in the same directory as the config file.
|
||||
|
||||
* `*.swift` - Matches any Swift file in the same directory as the config file.
|
||||
|
||||
* `foo/bar.swift` - Matches the file "bar.swift" in the directory "foo".
|
||||
|
||||
* `**/foo.swift` - Matches any file named "foo.swift" in the project.
|
||||
|
||||
* `**/*.swift` - Matches any Swift file in the project.
|
||||
|
||||
* `**/Generated` - Matches any folder called `Generated` in the project.
|
||||
|
||||
* `**/*_generated.swift` - Matches any Swift file with the suffix "_generated" in the project.
|
||||
|
||||
|
||||
Linting
|
||||
-------
|
||||
|
||||
SwiftFormat is primarily designed as a formatter rather than a linter, i.e. it is designed to fix your code rather than tell you what's wrong with it. However, sometimes it can be useful to verify that code has been formatted in a context where it is not desirable to actually change it.
|
||||
|
||||
A typical example would be as part of a CI (Continuous Integration) process, where you may wish to have an automated script that checks committed code for style violations. While you can use a separate tool such as [SwiftLint](https://github.com/realm/SwiftLint) for this, it makes sense to be able to validate the formatting against the exact same rules as you are using to apply it.
|
||||
|
||||
In order to run SwiftFormat as a linter, you can use the `--lint` command-line option:
|
||||
|
||||
```bash
|
||||
$ swiftformat --lint path/to/project
|
||||
```
|
||||
|
||||
This runs the same rules as format mode, and all the same configuration options apply, however, no files will be modified. Instead, SwiftFormat will format each file in memory and then compare the result against the input and report the lines that required changes.
|
||||
|
||||
The `--lint` option is similar to `--dryrun`, but `--lint` returns warnings for every line that required changes, and will return a nonzero error code (see [Error codes](#error-codes) below) if any changes are detected, which is useful if you want it to fail a build step on your CI server.
|
||||
|
||||
If you would prefer `--lint` not to fail your build, you can use the `--lenient` option to force SwiftFormat to return success in `--lint` mode even when formatting issues were detected.
|
||||
|
||||
```bash
|
||||
$ swiftformat --lint --lenient path/to/project
|
||||
```
|
||||
|
||||
By default, `--lint` will only report lines that require formatting, but you can use the additional `--verbose` flag to display additional info about which files were checked, even if there were no changes needed.
|
||||
|
||||
If you would prefer not to see a warning for each and every formatting change, you can use the `--quiet` flag to suppress all output except errors.
|
||||
|
||||
Sometimes you may wish to autoformat some rules, but only lint others. To do that, use the `--lintonly` option in your config file to specify rules that should only be applied in `--lint` mode:
|
||||
|
||||
```
|
||||
--rules braces,indent
|
||||
--lintonly trailingClosures,unusedArguments
|
||||
```
|
||||
|
||||
|
||||
Error codes
|
||||
-----------
|
||||
|
||||
The swiftformat command-line tool will always exit with one of the following codes:
|
||||
|
||||
* 0 - Success. This code will be returned in the event of a successful formatting run or if `--lint` detects no violations.
|
||||
* 1 - Lint failure. This code will be returned only when running in `--lint` mode if the input requires formatting.
|
||||
* 70 - Program error. This code will be returned if there is a problem with the input or configuration arguments.
|
||||
|
||||
|
||||
Cache
|
||||
------
|
||||
|
||||
SwiftFormat uses a cache file to avoid reformatting files that haven't changed. For a large project, this can significantly reduce processing time.
|
||||
|
||||
By default, the cache is stored in `~/Library/Caches/com.charcoaldesign.swiftformat` on macOS, or `/var/tmp/com.charcoaldesign.swiftformat` on Linux. Use the command-line option `--cache ignore` to ignore the cached version and re-apply formatting to all files. Alternatively, you can use `--cache clear` to delete the cache (or you can just manually delete the cache file).
|
||||
|
||||
The cache is shared between all projects. The file is fairly small, as it only stores the path and size for each file, not the contents. If you do start experiencing slowdown due to the cache growing too large, you might want to consider using a separate cache file for each project.
|
||||
|
||||
You can specify a custom cache file location by passing a path as the `--cache` option value. For example, you might want to store the cache file inside your project directory. It is fine to check in the cache file if you want to share it between different users of your project, as the paths stored in the cache are relative to the location of the formatted files.
|
||||
|
||||
|
||||
File headers
|
||||
-------------
|
||||
|
||||
SwiftFormat can be configured to strip or replace the header comments in every file with a template. The "header comment" is defined as a comment block that begins on the first nonblank line in the file, and is followed by at least one blank line. This may consist of a single comment body, or multiple comments on consecutive lines:
|
||||
|
||||
```swift
|
||||
// This is a header comment
|
||||
```
|
||||
|
||||
```swift
|
||||
// This is a regular comment
|
||||
func foo(bar: Int) -> Void { ... }
|
||||
```
|
||||
|
||||
The header template is a string that you provide using the `--header` command-line option. Passing a value of `ignore` (the default) will leave the header comments unmodified. Passing `strip` or an empty string `""` will remove them. If you wish to provide a custom header template, the format is as follows:
|
||||
|
||||
For a single-line template: `--header "Copyright (c) 2017 Foobar Industries"`
|
||||
|
||||
For a multiline comment, mark linebreaks with `\n`: `--header "First line\nSecond line"`
|
||||
|
||||
You can optionally include Swift comment markup in the template if you wish: `--header "/*--- Header comment ---*/"`
|
||||
|
||||
If you do not include comment markup, each line in the template will be prepended with `//` and a single space.
|
||||
|
||||
It is common practice to include the file name, creation date and/or the current year in a comment header copyright notice. To do that, you can use the following placeholders:
|
||||
|
||||
* `{file}` - the name of the file
|
||||
* `{year}` - the current year
|
||||
* `{created}` - the date on which the file was created
|
||||
* `{created.year}` - the year in which the file was created
|
||||
|
||||
For example, a header template of:
|
||||
|
||||
```bash
|
||||
--header "{file}\nCopyright (c) {year} Foobar Industries\nCreated by John Smith on {created}."
|
||||
```
|
||||
|
||||
Will be formatted as:
|
||||
|
||||
```swift
|
||||
// SomeFile.swift
|
||||
// Copyright (c) 2019 Foobar Industries
|
||||
// Created by John Smith on 01/02/2016.
|
||||
```
|
||||
|
||||
**NOTE:** the `{year}` value and `{created}` date format are determined from the current locale and timezone of the machine running the script.
|
||||
|
||||
|
||||
FAQ
|
||||
-----
|
||||
|
||||
*Q. How is this different from SwiftLint?*
|
||||
|
||||
> A. SwiftLint is primarily designed to find and report code smells and style violations in your code. SwiftFormat is designed to fix them. While SwiftLint can autocorrect some issues, and SwiftFormat has some support for [linting](#linting), their primary functions are different.
|
||||
|
||||
|
||||
*Q. Can SwiftFormat and SwiftLint be used together?*
|
||||
|
||||
> A. Absolutely! The style rules encouraged by both tools are quite similar, and SwiftFormat even fixes some style violations that SwiftLint warns about but can't currently autocorrect.
|
||||
|
||||
|
||||
*Q. What platforms does SwiftFormat support?*
|
||||
|
||||
> A. SwiftFormat works on macOS 10.13 (High Sierra) and above, and also runs on Ubuntu Linux and Windows.
|
||||
|
||||
|
||||
*Q. What versions of Swift are supported?*
|
||||
|
||||
> A. The SwiftFormat framework and command-line tool can be compiled using Swift 4.2 and above, and can format programs written in Swift 4.x or 5. Swift 3.x is no longer actively supported. If you are still using Swift 3.x or earlier and find that SwiftFormat breaks your code, the best solution is probably to revert to an earlier SwiftFormat release, or enable only a small subset of rules. Use the `--swiftversion` argument to enable additional rules specific to later Swift versions.
|
||||
|
||||
|
||||
*Q. SwiftFormat made changes I didn't want it to. How can I find out which rules to disable?*
|
||||
|
||||
> A. If you run SwiftFormat using the `--verbose` option, it will tell you which rules were applied to each file. You can then selectively disable certain rules using the `--disable` argument (see below).
|
||||
|
||||
|
||||
*Q. People on my team have different SwiftFormat versions installed. How can we ensure consistent formatting?
|
||||
|
||||
> A. You can specify a `--minversion` argument in your project's .swiftformat` file to fail the build if developers attempt to use an older SwiftFormat version.
|
||||
|
||||
|
||||
*Q. How can I modify the formatting rules?*
|
||||
|
||||
> A. Many configuration options are exposed in the command-line interface or `.swiftformat` configuration file. You can either set these manually, or use the `--inferoptions` argument to automatically generate the configuration from your existing project.
|
||||
|
||||
> If there is a rule that you don't like, and which cannot be configured to your liking via the command-line options, you can disable one or more rules by using the `--disable` argument, followed by the name of the rules, separated by commas. You can display a list of all supported rules using the `--rules` argument, and their behaviors are documented above this section in the README.
|
||||
|
||||
> If you are using the Xcode source editor extension, rules and options can be configured using the [SwiftFormat for Xcode](#xcode-source-editor-extension) host application. Unfortunately, due to limitation of the Extensions API, there is no way to configure these on a per-project basis.
|
||||
|
||||
> If the options you want aren't exposed, and disabling the rule doesn't solve the problem, the rules are implemented in the file `Rules.swift`, so you can modify them and build a new version of the command-line tool. If you think your changes might be generally useful, make a pull request.
|
||||
|
||||
|
||||
Q. I don't want to be surprised by new rules added when I upgrade SwiftFormat. How can I prevent this?
|
||||
|
||||
> A. You can use the `--rules` argument to specify an exclusive list of rules to run. If new rules are added, they won't be enabled if you have specified a `--rules` list in your SwiftFormat configuration.
|
||||
|
||||
|
||||
*Q. Why can't I set the indent width or choose between tabs/spaces in the [SwiftFormat for Xcode](#xcode-source-editor-extension) options?*
|
||||
|
||||
> Indent width and tabs/spaces can be configured in Xcode on a per project-basis. You'll find the option under "Text Settings" in the Files inspector of the right-hand sidebar.
|
||||
|
||||
|
||||
*Q. After applying SwiftFormat, my code won't compile. Is that a bug?*
|
||||
|
||||
> A. SwiftFormat should ideally never break your code. Check the [known issues](#known-issues), and if it's not already listed there, or the suggested workaround doesn't solve your problem, please [open an issue on Github](https://github.com/nicklockwood/SwiftFormat/issues).
|
||||
|
||||
|
||||
*Q. Can I use SwiftFormat to lint my code without changing it?*
|
||||
|
||||
> A. Yes, see the [linting](#linting) section above for details.
|
||||
|
||||
|
||||
*Q. Can I use the `SwiftFormat.framework` inside another app?*
|
||||
|
||||
> A. Yes, the SwiftFormat framework can be included in an app or test target, and used for many kinds of parsing and processing of Swift source code besides formatting. The SwiftFormat framework is available as a [CocoaPod](https://cocoapods.org/pods/SwiftFormat) for easy integration.
|
||||
|
||||
|
||||
Known issues
|
||||
---------------
|
||||
|
||||
* When using the Xcode Source Editor Extension, the SwiftFormat menu sometimes disappears from Xcode. If this happens, try moving or renaming Xcode temporarily and then changing it back. Failing that, the suggestions in [this thread](https://github.com/nicklockwood/SwiftFormat/issues/494) may help.
|
||||
|
||||
* The `enumNamespaces` rule replaces classes that have only static members with an `enum`. If the class is subclassed, or if there is code that depends on the class exposing certain runtime behaviors, this may break the program. To solve this you can either fix it on a per-case basis by adding a `// swiftformat:disable:next enumNamespaces` comment directive above the class declaration, or you can add `--enumnamespaces structs-only` to prevent the rule being applied to classes, or you can just disable the `enumNamespaces` rule completely.
|
||||
|
||||
* The `redundantVoidReturnType` rule can inadvertently alter the type signature for closures, for example in cases where the closure calls a `@discardableResult` function. To solve this you can either fix it on a per-case basis by adding a `// swiftformat:disable:next redundantVoidReturnType` comment directive to disable the rule for a specific call site, or you can add `--closurevoid preserve` to your [configuration](#configuration) to disable the rule completely for closures (regular functions or methods aren't affected).
|
||||
|
||||
* The `redundantType` rule can introduce ambiguous code in certain cases when using the default mode of `--redundanttype inferred`. This can be worked around by by using `--redundanttype explicit`, or by manually removing the redundant type reference on the affected line, or by using the `// swiftformat:disable:next redundantType` comment directive to disable the rule at the call site (or just disable the `redundantType` rule completely).
|
||||
|
||||
* If a type initializer or factory method returns an implicitly unwrapped optional value then the `redundantType` rule may remove the explicit type in a situation where it's actually required. To work around this you can either use `--redundanttype explicit`, or use the `// swiftformat:disable:next redundantType` comment directive to disable the rule at the call site (or just disable the `redundantType` rule completely).
|
||||
|
||||
* When using the `initCoderUnavailable` rule, if an `init` that is marked as unavailable is overridden elsewhere in the program then it will cause a compilation error. The recommended workaround is to remove the override (which shouldn't affect the program behavior if the init was really unused) or use the `// swiftformat:disable:next initCoderUnavailable` comment directive to disable the rule for the overridden init (or just disable the `initCoderUnavailable` rule completely).
|
||||
|
||||
* When using the `extensionAccessControl` rule with the `--extensionacl on-extension` option, if you have public methods defined on an internal type defined in another file, the resultant public extension will no longer compile. The recommended solution is to manually remove the `public` modifier (this won't change the program behavior) or disable the `extensionAccessControl` rule.
|
||||
|
||||
* When using the `preferKeyPath` rule, conversion of `compactMap { $0.foo }` to `compactMap(\.foo)` or `flatMap { $0.foo }` to `flatMap(\.foo)` will result in code that fails to compile if `foo` is not an `Optional` property. This is due to a difference in the way that Swift handles type inference for closures vs keyPaths, as discussed [here](https://bugs.swift.org/browse/SR-13347). The recommended workaround is to replace `compactMap()` or `flatMap()` with `map()` in these cases, which will not change the behavior of the code.
|
||||
|
||||
* When using the `--self remove` option, the `redundantSelf` rule will remove references to `self` in autoclosure arguments, which may change the meaning of the code, or cause it not to compile. To work around this issue, use the `--selfrequired` option to provide a comma-delimited list of methods to be excluded from the rule. The `expect()` function from the popular [Nimble](https://github.com/Quick/Nimble) unit testing framework is already excluded by default. If you are using the `--self insert` option then this is not an issue.
|
||||
|
||||
* If you assign `SomeClass.self` to a variable and then instantiate an instance of the class using that variable, Swift requires that you use an explicit `.init()`, however, the `redundantInit` rule is not currently capable of detecting this situation in all cases, and may remove the `.init`. To work around this issue, use the `// swiftformat:disable:next redundantInit` comment directive to disable the rule for any affected lines of code (or just disable the `redundantInit` rule completely).
|
||||
|
||||
* The `--self insert` option can only recognize locally declared member variables, not ones inherited from superclasses or extensions in other files, so it cannot insert missing `self` references for those. Note that the reverse is not true: `--self remove` should remove *all* redundant `self` references.
|
||||
|
||||
* The `trailingClosures` rule can generate ambiguous code if a function has multiple optional closure arguments, or if multiple functions have signatures differing only by the name of the closure argument. For this reason, the rule is limited to anonymous closure arguments by default. You can use the `--trailingclosures` and `--nevertrailing` arguments to explicitly opt in or out of trailing closure support for specific functions.
|
||||
|
||||
* The `isEmpty` rule will convert `count == 0` to `isEmpty` even for types that do not have an `isEmpty` method, such as `NSArray`/`NSDictionary`/etc. Use of Foundation collections in Swift code is pretty rare, but just in case, the rule is disabled by default.
|
||||
|
||||
* If a file begins with a comment, the `stripHeaders` rule will remove it if it is followed by a blank line. To avoid this, make sure that the first comment is directly followed by a line of code.
|
||||
|
||||
* When running a version of SwiftFormat built using Xcode 10.2 on macOS 10.14.3 or earlier, you may experience a crash with the error "dyld: Library not loaded: @rpath/libswiftCore.dylib". To fix this, you need to install the [Swift 5 Runtime Support for Command Line Tools](https://support.apple.com/kb/DL1998). These tools are included by default in macOS 10.14.4 and later.
|
||||
|
||||
|
||||
Tip Jar
|
||||
-----------
|
||||
|
||||
SwiftFormat is not a commercially-funded product, it's a labor of love given freely to the community. If you find it useful, please consider making a donation.
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9ZGWNK5FEZFF6&source=url)
|
||||
|
||||
|
||||
Credits
|
||||
------------
|
||||
|
||||
* [Tony Arnold](https://github.com/tonyarnold) - SwiftFormat for Xcode
|
||||
* [Vincent Bernier](https://github.com/vinceburn) - SwiftFormat for Xcode settings UI
|
||||
* [Vikram Kriplaney](https://github.com/markiv) - SwiftFormat for Xcode icon and search feature
|
||||
* [Hyperphonic](https://github.com/hyperphonic0) - Xcode 12 compatibility for SwiftFormat
|
||||
* [Maxime Marinel](https://github.com/bourvill) - Git pre-commit hook script
|
||||
* [Romain Pouclet](https://github.com/palleas) - Homebrew formula
|
||||
* [Aerobounce](https://github.com/aerobounce) - Homebrew cask and Sublime Text plugin
|
||||
* [Cal Stephens](https://github.com/calda) - Several new formatting rules and options
|
||||
* [Facundo Menzella](https://github.com/acumenzella) - Several new formatting rules
|
||||
* [Ali Akhtarzada](https://github.com/aliak00) - Several path-related CLI enhancements
|
||||
* [Yonas Kolb](https://github.com/yonaskolb) - Swift Package Manager integration
|
||||
* [Wolfgang Lutz](https://github.com/Lutzifer) - AppleScript integration instructions
|
||||
* [Balázs Kilvády](https://github.com/balitm) - Xcode lint warning integration
|
||||
* [Anthony Miller](https://github.com/AnthonyMDev) - Improvements to wrap/indent logic
|
||||
* [Shingo Takagi](https://github.com/zizi4n5) - Several brace-related bug fixes
|
||||
* [Benedek Kozma](https://github.com/cyberbeni) - Lint-only rules option
|
||||
* [Juri Pakaste](https://github.com/juri) - Filelist feature
|
||||
* [Jim Puls](https://github.com/puls) - Big Sur icon update
|
||||
* [Daniele Formichelli](https://github.com/danyf90) - JSON reporter
|
||||
* [Mahdi Bchatnia](https://github.com/inket) - Linux build workflow
|
||||
* [Arthur Semenyutin](https://github.com/vox-humana) - Docker image
|
||||
* [Nick Lockwood](https://github.com/nicklockwood) - Everything else
|
||||
|
||||
([Full list of contributors](https://github.com/nicklockwood/SwiftFormat/graphs/contributors))
|
||||
Generated
-21
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Realm Inc.
|
||||
|
||||
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.
|
||||
Generated
BIN
Binary file not shown.
-26
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
Generated
-24
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
${PODS_ROOT}/Target Support Files/GradientLoadingBar/GradientLoadingBar-Unit-Tests-frameworks.sh
|
||||
${BUILT_PRODUCTS_DIR}/GradientLoadingBar/GradientLoadingBar.framework
|
||||
${BUILT_PRODUCTS_DIR}/SnapshotTesting/SnapshotTesting.framework
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GradientLoadingBar.framework
|
||||
${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapshotTesting.framework
|
||||
Generated
-188
@@ -1,188 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
function on_error {
|
||||
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then
|
||||
# If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy
|
||||
# frameworks to, so exit 0 (signalling the script phase was successful).
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
|
||||
COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}"
|
||||
SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}"
|
||||
BCSYMBOLMAP_DIR="BCSymbolMaps"
|
||||
|
||||
|
||||
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||
|
||||
# Copies and strips a vendored framework
|
||||
install_framework()
|
||||
{
|
||||
if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
|
||||
local source="${BUILT_PRODUCTS_DIR}/$1"
|
||||
elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
|
||||
local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
|
||||
elif [ -r "$1" ]; then
|
||||
local source="$1"
|
||||
fi
|
||||
|
||||
local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
||||
|
||||
if [ -L "${source}" ]; then
|
||||
echo "Symlinked..."
|
||||
source="$(readlink "${source}")"
|
||||
fi
|
||||
|
||||
if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then
|
||||
# Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied
|
||||
find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do
|
||||
echo "Installing $f"
|
||||
install_bcsymbolmap "$f" "$destination"
|
||||
rm "$f"
|
||||
done
|
||||
rmdir "${source}/${BCSYMBOLMAP_DIR}"
|
||||
fi
|
||||
|
||||
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}"
|
||||
|
||||
local basename
|
||||
basename="$(basename -s .framework "$1")"
|
||||
binary="${destination}/${basename}.framework/${basename}"
|
||||
|
||||
if ! [ -r "$binary" ]; then
|
||||
binary="${destination}/${basename}"
|
||||
elif [ -L "${binary}" ]; then
|
||||
echo "Destination binary is symlinked..."
|
||||
dirname="$(dirname "${binary}")"
|
||||
binary="${dirname}/$(readlink "${binary}")"
|
||||
fi
|
||||
|
||||
# Strip invalid architectures so "fat" simulator / device frameworks work on device
|
||||
if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then
|
||||
strip_invalid_archs "$binary"
|
||||
fi
|
||||
|
||||
# Resign the code if required by the build settings to avoid unstable apps
|
||||
code_sign_if_enabled "${destination}/$(basename "$1")"
|
||||
|
||||
# Embed linked Swift runtime libraries. No longer necessary as of Xcode 7.
|
||||
if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then
|
||||
local swift_runtime_libs
|
||||
swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u)
|
||||
for lib in $swift_runtime_libs; do
|
||||
echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
|
||||
rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
|
||||
code_sign_if_enabled "${destination}/${lib}"
|
||||
done
|
||||
fi
|
||||
}
|
||||
# Copies and strips a vendored dSYM
|
||||
install_dsym() {
|
||||
local source="$1"
|
||||
warn_missing_arch=${2:-true}
|
||||
if [ -r "$source" ]; then
|
||||
# Copy the dSYM into the targets temp dir.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}"
|
||||
|
||||
local basename
|
||||
basename="$(basename -s .dSYM "$source")"
|
||||
binary_name="$(ls "$source/Contents/Resources/DWARF")"
|
||||
binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}"
|
||||
|
||||
# Strip invalid architectures from the dSYM.
|
||||
if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then
|
||||
strip_invalid_archs "$binary" "$warn_missing_arch"
|
||||
fi
|
||||
if [[ $STRIP_BINARY_RETVAL == 0 ]]; then
|
||||
# Move the stripped file into its final destination.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}"
|
||||
else
|
||||
# The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing.
|
||||
mkdir -p "${DWARF_DSYM_FOLDER_PATH}"
|
||||
touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Used as a return value for each invocation of `strip_invalid_archs` function.
|
||||
STRIP_BINARY_RETVAL=0
|
||||
|
||||
# Strip invalid architectures
|
||||
strip_invalid_archs() {
|
||||
binary="$1"
|
||||
warn_missing_arch=${2:-true}
|
||||
# Get architectures for current target binary
|
||||
binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)"
|
||||
# Intersect them with the architectures we are building for
|
||||
intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)"
|
||||
# If there are no archs supported by this binary then warn the user
|
||||
if [[ -z "$intersected_archs" ]]; then
|
||||
if [[ "$warn_missing_arch" == "true" ]]; then
|
||||
echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)."
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=1
|
||||
return
|
||||
fi
|
||||
stripped=""
|
||||
for arch in $binary_archs; do
|
||||
if ! [[ "${ARCHS}" == *"$arch"* ]]; then
|
||||
# Strip non-valid architectures in-place
|
||||
lipo -remove "$arch" -output "$binary" "$binary"
|
||||
stripped="$stripped $arch"
|
||||
fi
|
||||
done
|
||||
if [[ "$stripped" ]]; then
|
||||
echo "Stripped $binary of architectures:$stripped"
|
||||
fi
|
||||
STRIP_BINARY_RETVAL=0
|
||||
}
|
||||
|
||||
# Copies the bcsymbolmap files of a vendored framework
|
||||
install_bcsymbolmap() {
|
||||
local bcsymbolmap_path="$1"
|
||||
local destination="${BUILT_PRODUCTS_DIR}"
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"
|
||||
}
|
||||
|
||||
# Signs a framework with the provided identity
|
||||
code_sign_if_enabled() {
|
||||
if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then
|
||||
# Use the current code_sign_identity
|
||||
echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
|
||||
local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'"
|
||||
|
||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||
code_sign_cmd="$code_sign_cmd &"
|
||||
fi
|
||||
echo "$code_sign_cmd"
|
||||
eval "$code_sign_cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/GradientLoadingBar/GradientLoadingBar.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SnapshotTesting/SnapshotTesting.framework"
|
||||
fi
|
||||
if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/GradientLoadingBar/GradientLoadingBar.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/SnapshotTesting/SnapshotTesting.framework"
|
||||
fi
|
||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||
wait
|
||||
fi
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface PodsDummy_GradientLoadingBar : NSObject
|
||||
@end
|
||||
@implementation PodsDummy_GradientLoadingBar
|
||||
@end
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
FOUNDATION_EXPORT double GradientLoadingBarVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char GradientLoadingBarVersionString[];
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/GradientLoadingBar
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/../..
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
framework module GradientLoadingBar {
|
||||
umbrella header "GradientLoadingBar-umbrella.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/GradientLoadingBar
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/../..
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${PODS_CONFIGURATION_BUILD_DIR}/GradientLoadingBar" "${PODS_CONFIGURATION_BUILD_DIR}/SnapshotTesting"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -framework "GradientLoadingBar" -framework "SnapshotTesting" -framework "XCTest"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/../..
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${PODS_CONFIGURATION_BUILD_DIR}/GradientLoadingBar" "${PODS_CONFIGURATION_BUILD_DIR}/SnapshotTesting"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift "$(PLATFORM_DIR)/Developer/Library/Frameworks" '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -framework "GradientLoadingBar" -framework "SnapshotTesting" -framework "XCTest"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/../..
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
# Acknowledgements
|
||||
This application makes use of the following third party libraries:
|
||||
|
||||
## GradientLoadingBar
|
||||
|
||||
Copyright (c) 2017-2020 Felix Mau <me@felix.hamburg>
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## SwiftConfigurationFiles
|
||||
|
||||
Copyright (c) 2020 Felix Mau <me@felix.hamburg>
|
||||
|
||||
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.
|
||||
|
||||
## SwiftFormat
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Nick Lockwood
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## SwiftLint
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Realm Inc.
|
||||
|
||||
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.
|
||||
|
||||
Generated by CocoaPods - https://cocoapods.org
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user