-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit dd07a03
Showing
12 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.swiftpm | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"object": { | ||
"pins": [ | ||
{ | ||
"package": "SnapshotTesting", | ||
"repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", | ||
"state": { | ||
"branch": null, | ||
"revision": "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", | ||
"version": "1.9.0" | ||
} | ||
} | ||
] | ||
}, | ||
"version": 1 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// swift-tools-version:5.3 | ||
// The swift-tools-version declares the minimum version of Swift required to build this package. | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "swift-snapshot-testing-stitch", | ||
platforms: [ | ||
.iOS(.v11), | ||
], | ||
products: [ | ||
.library( | ||
name: "swift-snapshot-testing-stitch", | ||
targets: ["SnapshotTestingStitch"] | ||
), | ||
], | ||
dependencies: [ | ||
.package(name: "SnapshotTesting", | ||
url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.8.0"), | ||
], | ||
targets: [ | ||
.target( | ||
name: "SnapshotTestingStitch", | ||
dependencies: [ | ||
.product(name: "SnapshotTesting", package: "SnapshotTesting"), | ||
] | ||
), | ||
|
||
.testTarget( | ||
name: "SnapshotTestingStitchTests", | ||
dependencies: ["SnapshotTestingStitch"], | ||
exclude: ["__Snapshots__"] | ||
), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# SnapshotTesting Stitch | ||
|
||
This is an extension to [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) which allows you to create images combining the output of multiple snapshot strategies assuming they all output to UIImage. | ||
|
||
In essence, this allows you to have a single image which represents a single snapshotted value in multiple different configurations. This might be useful in situations, for example, where you have the same UIViewController and want a single image showing the view in multiple sizes. | ||
|
||
Images may also have titles, allowing you to easily identify each configuration within the image. | ||
|
||
## Usage | ||
|
||
See tests for example usage. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import UIKit | ||
import SnapshotTesting | ||
|
||
extension Snapshotting where Value == UIViewController, Format == UIImage { | ||
|
||
public static func stitch( | ||
strategies: [Snapshotting<Value, Format>], | ||
style: StitchStyle = .init() | ||
) -> Snapshotting { | ||
// Default to an empty string, if they choose not to provide one. | ||
stitch(strategies: strategies.map { ("", $0) }, style: style) | ||
} | ||
|
||
public static func stitch( | ||
strategies: [(name: String, strategy: Snapshotting<Value, Format>)], | ||
style: StitchStyle = .init() | ||
) -> Snapshotting { | ||
let internalStrategy: Snapshotting<UIViewController, UIImage> = .image | ||
|
||
return Snapshotting( | ||
pathExtension: internalStrategy.pathExtension, | ||
diffing: internalStrategy.diffing | ||
) { value in | ||
Async<UIImage> { callback in | ||
// Create a dispatch group to keep track of the remaining tasks | ||
let dispatchGroup = DispatchGroup() | ||
|
||
// Create an array to store the final outputs to be stitched | ||
var values = [(String, UIImage)]() | ||
|
||
// Loop over each of the user-provided strategies, snapshot them, | ||
// store the output, and update the dispatch group. | ||
strategies.forEach { strategy in | ||
dispatchGroup.enter() | ||
|
||
strategy.strategy.snapshot(value).run { output in | ||
values.append((strategy.name, output)) | ||
dispatchGroup.leave() | ||
} | ||
} | ||
|
||
// Once all strategies have been completed... | ||
dispatchGroup.notify(queue: .main) { | ||
// Sort values based on input order | ||
let comparableValues = strategies.compactMap { (name, _) in | ||
values.first(where: { $0.0 == name }) | ||
} | ||
|
||
// Stitch them together, and callback to the snapshot testing library. | ||
let image = ImageStitcher(inputs: comparableValues).stitch(style: style) | ||
callback(image) | ||
} | ||
} | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import UIKit | ||
|
||
public struct StitchStyle { | ||
/// How large should the font size be for the optional titles which appear above each stitched image? | ||
public let fontSize: CGFloat | ||
|
||
/// What color should the border be which surrounds each individual stitched image? | ||
public let borderColor: UIColor | ||
|
||
/// How thick should the border be which surrounds each individual stitched image? | ||
public let borderWidth: CGFloat | ||
|
||
/// How far apart should each stitched image be from another? | ||
public let itemSpacing: CGFloat | ||
|
||
/// Creates a defintion of how a stitched snapshot should be presented. | ||
/// | ||
/// Allows you to customise certain aspects of the stitched output image. | ||
/// | ||
/// - Parameters: | ||
/// - fontSize: How large should the font size be for the optional titles which appear above each stitched image? | ||
/// - borderColor: What color should the border be which surrounds each individual stitched image? | ||
/// - borderWidth: How thick should the border be which surrounds each individual stitched image? | ||
/// - itemSpacing: How far apart should each stitched image be from another? | ||
public init( | ||
fontSize: CGFloat = 20, | ||
borderColor: UIColor = .red, | ||
borderWidth: CGFloat = 5, | ||
itemSpacing: CGFloat = 32 | ||
) { | ||
assert(borderWidth >= 0, "The provided border width should be a positive integer, or zero if you do not want a border to be displayed.") | ||
assert(itemSpacing >= 0, "The provided item spacing should be a positive integer.") | ||
assert(fontSize >= 0, "The provided font size should be a positive integer.") | ||
|
||
self.fontSize = fontSize | ||
self.borderColor = borderColor | ||
self.borderWidth = borderWidth | ||
self.itemSpacing = itemSpacing | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import UIKit | ||
|
||
struct ImageStitcher { | ||
|
||
let inputs: [(title: String, image: UIImage)] | ||
|
||
func stitch(style: StitchStyle) -> UIImage { | ||
|
||
let includeTitles: Bool = inputs.map { $0.title }.allSatisfy { $0 == "" } == false | ||
let images = inputs.map { $0.image } | ||
|
||
// Calculate how large the full image will be based on the inputs | ||
let framePadding: CGFloat = style.itemSpacing | ||
|
||
let largestHeight = images.map { $0.size.height }.max() ?? 0 | ||
let computedHeight = | ||
largestHeight + | ||
(includeTitles ? style.fontSize + framePadding : 0) + // Title Size | ||
(style.borderWidth * 2) + // Vertical Border | ||
(framePadding * 2) // Vertical Padding | ||
|
||
let imageSumWidth = images.map { | ||
$0.size.width + | ||
(style.borderWidth * 2) + // Horizontal Border | ||
style.itemSpacing // Horizontal Interitem Spacing | ||
}.reduce(0, +) | ||
|
||
let computedWidth = | ||
imageSumWidth + | ||
(framePadding * 2) - // Horizontal Padding | ||
style.itemSpacing // Remove Final Image's Interitem Spacing | ||
|
||
// Create renderer with correct image size | ||
let computedSize = CGSize( | ||
width: computedWidth, | ||
height: computedHeight | ||
) | ||
|
||
let renderer = UIGraphicsImageRenderer(size: computedSize) | ||
|
||
// Setup title styling | ||
let paragraphStyle = NSMutableParagraphStyle() | ||
paragraphStyle.alignment = .center | ||
|
||
let titleAttributes: [NSAttributedString.Key: Any] = [ | ||
.font: UIFont.systemFont(ofSize: style.fontSize), | ||
.paragraphStyle: paragraphStyle, | ||
.foregroundColor: UIColor.white | ||
] | ||
|
||
// Draw the image | ||
let image = renderer.image { context in | ||
context.cgContext.setFillColor(UIColor.black.cgColor) | ||
context.cgContext.fill(CGRect(origin: .zero, size: computedSize)) | ||
|
||
var xOffset = framePadding + style.borderWidth | ||
var yOffset = framePadding + style.borderWidth | ||
|
||
// If we have a title to display, then we'll increase the yOffset to leave space at the top | ||
if includeTitles { | ||
yOffset += style.fontSize + framePadding | ||
} | ||
|
||
inputs.forEach { (title, image) in | ||
// Draw Border | ||
context.cgContext.setFillColor(style.borderColor.cgColor) | ||
context.cgContext.setStrokeColor(style.borderColor.cgColor) | ||
context.cgContext.setLineWidth(style.borderWidth) | ||
|
||
context.cgContext.addRect(CGRect( | ||
x: xOffset, | ||
y: yOffset, | ||
width: image.size.width + (style.borderWidth * 2), | ||
height: image.size.height + (style.borderWidth * 2) | ||
)) | ||
|
||
context.cgContext.drawPath(using: .fillStroke) | ||
|
||
// Draw Image | ||
image.draw(at: CGPoint( | ||
x: xOffset + style.borderWidth, | ||
y: yOffset + style.borderWidth | ||
)) | ||
|
||
// Draw Title | ||
if title.isEmpty == false { | ||
title.draw( | ||
with: CGRect( | ||
x: xOffset, | ||
y: framePadding, | ||
width: image.size.width, | ||
height: image.size.height | ||
), | ||
options: .usesLineFragmentOrigin, | ||
attributes: titleAttributes, | ||
context: nil | ||
) | ||
} | ||
|
||
// Increment horizontal offset for next image | ||
xOffset += | ||
image.size.width + | ||
style.itemSpacing + | ||
(style.borderWidth * 2) | ||
} | ||
} | ||
|
||
return image | ||
} | ||
|
||
} |
76 changes: 76 additions & 0 deletions
76
Tests/SnapshotTestingStitchTests/SnapshotTestingStitch.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import XCTest | ||
import SnapshotTesting | ||
@testable import SnapshotTestingStitch | ||
|
||
final class SnapshotTestingStitchTests: XCTestCase { | ||
|
||
let isRecording: Bool = false | ||
|
||
func test_withTitles() { | ||
assertSnapshot( | ||
matching: createTestViewController(), | ||
as: .stitch( | ||
strategies: [ | ||
("iPhone 8", .image(on: .iPhone8)), | ||
("iPhone 8 Plus", .image(on: .iPhone8Plus)), | ||
] | ||
), | ||
record: isRecording | ||
) | ||
} | ||
|
||
func test_withoutTitles() { | ||
assertSnapshot( | ||
matching: createTestViewController(), | ||
as: .stitch( | ||
strategies: [ | ||
.image(on: .iPhone8), | ||
.image(on: .iPhone8Plus), | ||
] | ||
), | ||
record: isRecording | ||
) | ||
} | ||
|
||
func test_withoutBorder() { | ||
assertSnapshot( | ||
matching: createTestViewController(), | ||
as: .stitch( | ||
strategies: [ | ||
("iPhone 8", .image(on: .iPhone8)), | ||
("iPhone 8 Plus", .image(on: .iPhone8Plus)), | ||
], | ||
style: .init(borderWidth: 0) | ||
), | ||
record: isRecording | ||
) | ||
} | ||
|
||
func test_withManyDevices() { | ||
assertSnapshot( | ||
matching: createTestViewController(), | ||
as: .stitch( | ||
strategies: [ | ||
("iPhone 8", .image(on: .iPhone8)), | ||
("iPhone 8 Plus", .image(on: .iPhone8Plus)), | ||
("iPhone X", .image(on: .iPhoneX)), | ||
("iPhone SE", .image(on: .iPhoneSe)), | ||
("iPhone Xr", .image(on: .iPhoneXr)), | ||
("iPhone Xs Max", .image(on: .iPhoneXsMax)), | ||
("iPhone Xs Max (Landscape)", .image(on: .iPhoneXsMax(.landscape))), | ||
] | ||
), | ||
record: isRecording | ||
) | ||
} | ||
|
||
// MARK: - Helpers | ||
|
||
func createTestViewController() -> UIViewController { | ||
let viewController = UIViewController() | ||
viewController.view.backgroundColor = .blue | ||
|
||
return viewController | ||
} | ||
|
||
} |
Binary file added
BIN
+436 KB
...stingStitchTests/__Snapshots__/SnapshotTestingStitch/test_withManyDevices.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+108 KB
...hotTestingStitchTests/__Snapshots__/SnapshotTestingStitch/test_withTitles.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+82.8 KB
...TestingStitchTests/__Snapshots__/SnapshotTestingStitch/test_withoutBorder.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+85.2 KB
...TestingStitchTests/__Snapshots__/SnapshotTestingStitch/test_withoutTitles.1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.