Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AsyncOperationTracer #391

Merged
merged 11 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.8
// swift-tools-version:5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -8,8 +8,6 @@ let package = Package(
platforms: [
.iOS(.v15),
.macOS(.v13),
.tvOS(.v14),
.watchOS(.v7)
],
products: [
.library(
Expand Down
5 changes: 5 additions & 0 deletions Sources/BSWInterfaceKit/Model/Photo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,8 @@ extension CGSize: Hashable { // For some reason `CGSize` isn't `Hashable`
extension Photo: Equatable, Hashable {}
extension Photo.Kind: Equatable, Hashable {}
extension Photo.PlaceholderImage: Equatable, Hashable {}


#if canImport(AppKit)
extension PlatformImage: @unchecked Sendable {}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

/// Hook this up in in `AppDelegate.didFinishLaunching`
/// to your preferred tacking framework in order to trace Async operations
public enum AsyncOperationTracer {

public static nonisolated func setOperationDidBegin(_ op: @escaping OperationHandler) {
Task { @AsyncOperationTracerStorageActor in
AsyncOperationTracer.operationDidBegin = op
}
}

public static nonisolated func setOperationDidEnd(_ op: @escaping OperationHandler) {
Task { @AsyncOperationTracerStorageActor in
AsyncOperationTracer.operationDidEnd = op
}
}

public static nonisolated func setOperationDidFail(_ op: @escaping OperationFailedHandler) {
Task { @AsyncOperationTracerStorageActor in
AsyncOperationTracer.operationDidFail = op
}
}

public typealias OperationHandler = @Sendable (Operation) async -> ()
public typealias OperationFailedHandler = @Sendable (Operation, any Error) async -> ()

public struct Operation: Sendable {

public let kind: Kind
public let id: any (Equatable & Sendable)

public enum Kind: Sendable {
case viewLoading
case buttonAction
}

public var traceValue: String {
switch self.kind {
case .buttonAction:
return "async-button-\(self.id)"
case .viewLoading:
return "async-view-\(self.id)"
}
}
}

@AsyncOperationTracerStorageActor
static var operationDidBegin: OperationHandler = { _ in }
@AsyncOperationTracerStorageActor
static var operationDidEnd: OperationHandler = { _ in }

@AsyncOperationTracerStorageActor
static var operationDidFail: OperationFailedHandler = { _, _ in }
}

@globalActor actor AsyncOperationTracerStorageActor: GlobalActor {
static let shared = AsyncOperationTracerStorageActor()
}
33 changes: 33 additions & 0 deletions Sources/BSWInterfaceKit/SwiftUI/Views/AsyncButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,20 @@ public struct AsyncButton<Label: View>: View {
}
#endif
let result: Swift.Result<Void, Swift.Error> = await {
if let operation = operation {
await AsyncOperationTracer.operationDidBegin(operation)
}
do {
try await action()
if let operation = operation {
await AsyncOperationTracer.operationDidEnd(operation)
}
return .success(())
} catch {
if let operation = operation {
await AsyncOperationTracer.operationDidEnd(operation)
await AsyncOperationTracer.operationDidFail(operation, error)
}
return .failure(error)
}
}()
Expand Down Expand Up @@ -125,6 +135,16 @@ public struct AsyncButton<Label: View>: View {
.ignoresSafeArea()
}
}

@Environment(\.asyncButtonOperationIdentifierKey)
private var operationKey

private var operation: AsyncOperationTracer.Operation? {
guard let operationKey else {
return nil
}
return .init(kind: .buttonAction, id: operationKey)
}

#if canImport(UIKit)
@MainActor
Expand Down Expand Up @@ -205,18 +225,31 @@ public extension View {
func asyncButtonLoadingConfiguration(message: String? = nil, style: AsyncButtonLoadingConfiguration.Style = .nonblocking) -> some View {
self.environment(\.asyncButtonLoadingConfiguration, .init(message: message, style: style))
}

func asyncButtonOperationIdentifierKey(_ key: String) -> some View {
self.environment(\.asyncButtonOperationIdentifierKey, key)
}
}

private struct AsyncButtonLoadingStyleEnvironmentKey: EnvironmentKey {
static let defaultValue: AsyncButtonLoadingConfiguration = .init()
}

private struct AsyncButtonOperationIdentifierKey: EnvironmentKey {
static let defaultValue: String? = nil
}

private extension EnvironmentValues {

var asyncButtonLoadingConfiguration: AsyncButtonLoadingConfiguration {
get { self[AsyncButtonLoadingStyleEnvironmentKey.self] }
set { self[AsyncButtonLoadingStyleEnvironmentKey.self] = newValue }
}

var asyncButtonOperationIdentifierKey: String? {
get { self[AsyncButtonOperationIdentifierKey.self] }
set { self[AsyncButtonOperationIdentifierKey.self] = newValue }
}
}

private extension Swift.Result where Failure == Error {
Expand Down
7 changes: 6 additions & 1 deletion Sources/BSWInterfaceKit/SwiftUI/Views/AsyncView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ struct RecipeListView: View, PlaceholderDataProvider {
/// To do so, implement `generatePlaceholderData()` from `PlaceholderDataProvider` protocol
///
@MainActor
public struct AsyncView<Data: Sendable, HostedView: View, ErrorView: View, LoadingView: View, ID: Equatable>: View {
public struct AsyncView<Data: Sendable, HostedView: View, ErrorView: View, LoadingView: View, ID: Equatable & Sendable>: View {

/// Represents the state of this view
struct Operation {
Expand Down Expand Up @@ -165,6 +165,9 @@ public struct AsyncView<Data: Sendable, HostedView: View, ErrorView: View, Loadi
withAnimation {
currentOperation.phase = .loading
}

let operation = AsyncOperationTracer.Operation(kind: .viewLoading, id: id)
await AsyncOperationTracer.operationDidBegin(operation)
do {
let finalData = try await dataGenerator()
withAnimation {
Expand All @@ -175,10 +178,12 @@ public struct AsyncView<Data: Sendable, HostedView: View, ErrorView: View, Loadi
} catch let error where error.isURLCancelled {
/// Do nothing as we handle this `.task`
} catch {
await AsyncOperationTracer.operationDidFail(operation, error)
withAnimation {
currentOperation.phase = .error(error)
}
}
await AsyncOperationTracer.operationDidEnd(operation)
}
}

Expand Down
8 changes: 8 additions & 0 deletions Tests/BSWInterfaceKitTests/Errors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

import Foundation

struct SomeError: LocalizedError {
var errorDescription: String? {
return "Some Error"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class UIViewControllerTaskTests: BSWSnapshotTest {
}

func testTaskErrorView() {
let vc = MockVC(taskGenerator: { throw "Some Error" })
let vc = MockVC(taskGenerator: { throw SomeError() })
waitABitAndVerify(viewController: vc, testDarkMode: false)
}

Expand Down
6 changes: 1 addition & 5 deletions Tests/BSWInterfaceKitTests/Suite/UIViewControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class UIViewControllerTests: BSWSnapshotTest {
let vc = TestViewController()
var buttonConfig = UIButton.Configuration.plain()
buttonConfig.title = "Retry"
vc.showErrorMessage("Something Failed", error: "Some Error", retryButton: buttonConfig)
vc.showErrorMessage("Something Failed", error: SomeError(), retryButton: buttonConfig)
waitABitAndVerify(viewController: vc, testDarkMode: false)
}

Expand Down Expand Up @@ -192,8 +192,4 @@ private class ContentVC: UIViewController {
}
}

extension String: LocalizedError {

}

#endif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading