Skip to content

Instantly share code, notes, and snippets.

@DavidBrunow
Last active November 2, 2023 22:19
Show Gist options
  • Save DavidBrunow/555c5b1f7fa364bbe69841ae439077e2 to your computer and use it in GitHub Desktop.
Save DavidBrunow/555c5b1f7fa364bbe69841ae439077e2 to your computer and use it in GitHub Desktop.
FlowRunner POC
import ComposableArchitecture
import SwiftUI
public struct FlowRunner {
public static func run<State: Equatable, Action: Equatable>(
type: String,
store: Store<State, Action>,
initialView: some View,
initializationAction: FlowRunner.NamedAction<Action>,
actions: [FlowRunner.NamedAction<Action>],
configurations: [FlowRunner.Configuration],
betweenActionsHandler: @MainActor @escaping (
UIViewController, UIUserInterfaceStyle, String
) -> Void
) async throws {
for configuration in configurations {
try await run(
type: type,
store: store,
initialView: initialView,
initializationAction: initializationAction,
actions: actions,
configuration: configuration,
betweenActionsHandler: betweenActionsHandler
)
}
}
private static func run<State: Equatable, Action: Equatable>(
type: String,
store: Store<State, Action>,
initialView: some View,
initializationAction: FlowRunner.NamedAction<Action>,
actions: [FlowRunner.NamedAction<Action>],
configuration: FlowRunner.Configuration,
betweenActionsHandler: @MainActor @escaping (
UIViewController, UIUserInterfaceStyle, String
) -> Void
) async throws {
let view = initialView
.environment(\.colorScheme, configuration.colorScheme)
.environment(\.dynamicTypeSize, configuration.dynamicTypeSize)
.environment(\.locale, configuration.locale)
.environment(
\.layoutDirection,
LayoutDirection.from(
Locale.Language(
identifier: configuration.locale.identifier
).characterDirection
)
)
.transaction { $0.animation = nil }
let hostingController = await UIHostingController(rootView: view)
let uiUserInterfaceStyle: UIUserInterfaceStyle = configuration.colorScheme == .dark ? .dark : .light
await betweenActionsHandler(
hostingController,
uiUserInterfaceStyle,
"\(configuration.name) \(type): Throwaway"
)
// Wait for SwiftUI to settle
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 1000)
await betweenActionsHandler(
hostingController,
uiUserInterfaceStyle,
"\(configuration.name) \(type): 01. \(initializationAction.name)"
)
let numberFormetter = NumberFormatter()
numberFormetter.minimumIntegerDigits = 2
for (index, action) in actions.enumerated() {
_ = await MainActor.run {
store.send(action.action)
}
guard let count = numberFormetter.string(from: index + 2 as NSNumber) else {
XCTFail("Could not format number")
continue
}
// Wait for SwiftUI to settle
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 600)
await betweenActionsHandler(
hostingController,
uiUserInterfaceStyle,
"\(configuration.name) \(type): \(count). \(action.name)"
)
}
// Wait a short bit between scenarios
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 500)
}
public struct NamedAction<Action> {
public let name: String
public let action: Action
public init(name: String, action: Action) {
self.name = name
self.action = action
}
}
public struct Configuration {
public var colorScheme: ColorScheme
public var dynamicTypeSize: DynamicTypeSize
public var locale: Locale
public var name: String
public init(
colorScheme: ColorScheme = .light,
dynamicTypeSize: DynamicTypeSize = .large,
locale: Locale = Locale(identifier: "en-US"),
name: String
) {
self.colorScheme = colorScheme
self.dynamicTypeSize = dynamicTypeSize
self.locale = locale
self.name = name
}
}
}
extension LayoutDirection {
fileprivate static func from(
_ localeLanguageDirection: Locale.LanguageDirection
) -> Self {
switch localeLanguageDirection {
case .unknown:
return .leftToRight
case .leftToRight:
return .leftToRight
case .rightToLeft:
return .rightToLeft
case .topToBottom:
fatalError("Top to bottom character directions not supported")
case .bottomToTop:
fatalError("Bottom to top character directions not supported")
@unknown default:
fatalError("Unknown language direction")
}
}
}
import SnapshotTesting
import XCTest
extension XCTest {
@MainActor
static func snapshotImage(
overrideUserInterfaceStyle: UIUserInterfaceStyle
) -> Snapshotting<UIViewController, UIImage> {
let perceptualPrecision: Float = 0.98
let precision: Float = 0.995
return .windowedImage(
precision: precision,
perceptualPrecision: perceptualPrecision,
overrideUserInterfaceStyle: overrideUserInterfaceStyle
)
}
}
extension Snapshotting where Value: UIViewController, Format == UIImage {
@MainActor
static func windowedImage(
precision: Float,
perceptualPrecision: Float,
overrideUserInterfaceStyle: UIUserInterfaceStyle
) -> Snapshotting {
SimplySnapshotting.image(
precision: precision,
perceptualPrecision: perceptualPrecision
).asyncPullback { viewController in
Async<UIImage> { callback in
// Hide carets in text fields
UITextField.appearance().tintColor = .clear
guard let window = UIApplication
.shared
.connectedScenes
.compactMap(
{ scene in
(scene as? UIWindowScene)?.keyWindow
}
)
.first
else {
fatalError("Cannot find key window")
}
// Try to speed up animations
window.layer.speed = 100
window.overrideUserInterfaceStyle = overrideUserInterfaceStyle
window.rootViewController = viewController
let image = UIGraphicsImageRenderer(bounds: window.bounds).image { _ in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
}
callback(image)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment