Replies: 35 comments 161 replies
-
no one: |
Beta Was this translation helpful? Give feedback.
-
How does deep-linking work on this beta branch? I've been playing around with it and things seem to work until I try to deep-link to a presentation state, but when I launch my app, nothing happens. struct CategoryListFeature: ReducerProtocol {
struct State: Equatable {
var categories: IdentifiedArrayOf<Category> = []
@PresentationState var selection: CategoryDetailFeature.State?
}
enum Action: Equatable {
case categoryTapped(Category)
case selection(PresentationAction<CategoryDetailFeature.Action>)
}
var body: some ReducerProtocol<State, Action> {
Reduce { ... }
.ifLet(\.$selection, action: /Action.selection) {
CategoryDetailFeature()
}
}
}
struct CategoryListView: View {
let store: StoreOf<CategoryListFeature>
var body: some View {
NavigationStack {
listView
.navigationTitle("Categories")
}
}
@ViewBuilder
private var listView: some View {
WithViewStore(store, observe: \.categories) { viewStore in
List { ... }
.navigationDestination(
store: self.store.scope(
state: \.$selection,
action: CategoryListFeature.Action.selection
),
destination: CategoryDetailView.init
)
}
}
struct ContentView: View {
var body: some View {
CategoryListView(
store: .init(
initialState: CategoryListFeature.State(
categories: [
.plumbing,
.electrical,
.hvac
],
selection: CategoryDetailFeature.State(category: .hvac)
),
reducer: CategoryListFeature()._printChanges()
)
)
}
}
When I try to initialize the store with a selection, the app does not navigate to the details screen, it stays on the list screen and then navigation is broken because selection is already non-nil, so when I tap on a list item, nothing happens either. |
Beta Was this translation helpful? Give feedback.
-
Is there a rough timeline for switching the prerelease 1.0 to track the new navigation beta? We recently made the switch over to it and would like to get to grips with the new navigation toys too. Thanks 🙏🏻 |
Beta Was this translation helpful? Give feedback.
-
In past discussions and episodes, you have encouraged us to represent our destination state as an enum. public struct MyFeature: Equatable {
@PresentationState public var destination: Destination?
public enum Destination: Equatable {
case featureA(FeatureA.State)
case featureB(FeatureB.State)
case alert(AlertState<Action>)
}
}
public enum Action {
case destination(PresentationAction<Destination>)
public enum Destination: Equatable {
case featureA(FeatureA.Action)
case featureB(FeatureB.Action)
}
} Is this currently possible to do this on the beta branch? Or will it be? I'm not sure how to switch on the destination using the new .ifLet func for |
Beta Was this translation helpful? Give feedback.
-
Just a quick check on intended usage. When using |
Beta Was this translation helpful? Give feedback.
-
For all the TCA Boundary fans out there (examples can be found here: #1440), this extension should prove useful: extension Reducer where Action: TCAAction {
public func ifLet<DestinationState, DestinationAction, Destination: ReducerProtocol>(
_ toPresentationState: WritableKeyPath<State, PresentationState<DestinationState>>,
action toPresentationAction: CasePath<
Action.InternalAction, PresentationAction<DestinationAction>
>,
@ReducerBuilder<DestinationState, DestinationAction> then destination: () -> Destination,
file: StaticString = #file,
fileID: StaticString = #fileID,
line: UInt = #line
) -> _PresentationReducer<Self, Destination>
where Destination.State == DestinationState, Destination.Action == DestinationAction {
return self.ifLet(
toPresentationState,
action: (/Action._internal) .. toPresentationAction,
then: destination
)
}
} |
Beta Was this translation helpful? Give feedback.
-
I'm so close! I imagined that I'd be able to use the new navigation tools like this in UIKit: class SettingsViewController: UIViewController {
...
func setupBindings() {
store.scope(
state: \.$fontManager,
action: /Action.fontManager
).ifLet { [weak self] store in // <= error happens here because .ifLet does not work on scope that is never nil
guard let self, let navigationController = self.navigationController else { return }
let viewController = FontManagerViewController(store: store)
navigationController.pushViewController(viewController, animated: true)
} else: { [weak self] _ in
guard let self, let navigationController = self.navigationController else { return }
navigationController.popToViewController(self, animated)
}
.store(in: &cancellables)
}
} However, the error arises because store.scope will never return a non-nil: Key path value type 'PresentationState<FontManager.State>' cannot
be converted to contextual type 'PresentationState<FontManager.State>?' Am I doing something wrong here or is there no support for UIKit at this time? |
Beta Was this translation helpful? Give feedback.
-
I don't know if this is a question on here or for TCA in general or even for the SwiftUI Navigation repo. 😅 There are a couple of instance of this pattern in our app at the moment but this is the easiest to explain. We have certain We created a In the navigation beta the general concept seems to be to use the state of the child to control the navigation. But... the state of the child would just be the same as the state of the parent. eg...
At the moment we have no actions or anything in the article (but we will in the future). So I could update this to ...
But then creating the I guess that's not too bad but I just wanted to sanity check myself on this. Is there a better way of doing this other than duplicating the data into a child state? Or is this ok to do? We wouldn't be editing the article in this case. Thanks |
Beta Was this translation helpful? Give feedback.
-
I have a failing test after moving to the navigation beta branch (I'm on prerelease/1.0) and I can't work out where it's coming from. The test is small. I send an action to the test store that creates a child state which is now It is failing telling me that there is an unfinished effect. The test...
Reducer...
Error...
|
Beta Was this translation helpful? Give feedback.
-
As you may have seen in recent episodes, it is often more flexible to contextualize a presented view at the presenting stage (where we have explicit information), rather than parametrizing the presented view with configuration flags. SwiftUI works mostly OK in this setup. Since iOS 13, UIKit ships with a dedicated dismiss control (the dark grey xmark inside a grey circle) under the form of a Fortunately we can bundle this button using a view representable in a very convenient You use this button like: .sheet(store.scope(…)) {
PresentedFeature(store: store)
.toolbar {
DismissButton()
}
} ( I'm not saying that this should ship with the library (it shouldn't in TCA for sure), but I guess this is something nice to have in our tool belt. Here is the code1: #if canImport(UIKit)
public struct DismissButton: View {
public init() {}
@Environment(\.dismiss) var dismiss
public var body: some View {
UICloseButton {
self.dismiss()
}
}
private struct UICloseButton: UIViewRepresentable {
let action: () -> Void
final class Coordinator {
var action: () -> Void
init(action: @escaping () -> Void) {
self.action = action
}
}
func makeCoordinator() -> Coordinator {
Coordinator(action: self.action)
}
func makeUIView(context: Context) -> some UIView {
UIButton(
type: .close,
primaryAction: .init(handler: { _ in
context.coordinator.action()
}))
}
func updateUIView(_ uiView: UIViewType, context: Context) {
context.coordinator.action = self.action
}
}
}
#endif Footnotes
|
Beta Was this translation helpful? Give feedback.
-
Is there a bug with alerts and confirmation dialogs or am I doing something wrong? Seems whenever I add a custom button/action to either one, when I click the button to send the action, I get this runtime warning:
Here is my simple code example: import ComposableArchitecture
import SwiftUI
struct AlertsAndDialogsFeature: ReducerProtocol {
struct State: Equatable {
@PresentationState var destination: Destinations.State?
}
enum Action: Equatable {
case destination(PresentationAction<Destinations.Action>)
case alertButtonTapped
case confirmationButtonTapped
}
var body: some ReducerProtocol<State, Action> {
Reduce(core)
.ifLet(\.$destination, action: /Action.destination) {
Destinations()
}
}
private func core(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case .alertButtonTapped:
state.destination = .alert(
AlertState {
TextState("Alert Dialog")
} actions: {
ButtonState(role: .cancel) {
TextState("Cancel")
}
ButtonState(action: Destinations.Action.Alert.alertConfirmButtonTapped) {
TextState("Confirm")
}
} message: {
TextState("Please confirm this alert")
}
)
return .none
case .confirmationButtonTapped:
state.destination = .confirmation(
ConfirmationDialogState {
TextState("Confirmation Dialog")
} actions: {
ButtonState(role: .cancel) {
TextState("Cancel")
}
ButtonState(action: Destinations.Action.Confirmation.confirmationConfirmButtonTapped) {
TextState("Confirm")
}
} message: {
TextState("Please confirm this confirmation dialog")
}
)
return .none
case .destination:
return .none
}
}
}
extension AlertsAndDialogsFeature {
struct Destinations: ReducerProtocol {
enum State: Equatable {
case alert(AlertState<Action.Alert>)
case confirmation(ConfirmationDialogState<Action.Confirmation>)
}
enum Action: Equatable {
case alert(Alert)
case confirmation(Confirmation)
enum Alert: Equatable {
case alertConfirmButtonTapped
}
enum Confirmation: Equatable {
case confirmationConfirmButtonTapped
}
}
var body: some ReducerProtocol<State, Action> {
Scope(state: /State.alert, action: /Action.alert) {}
Scope(state: /State.confirmation, action: /Action.confirmation) {}
}
}
}
struct AlertsAndDialogsView: View {
let store: StoreOf<AlertsAndDialogsFeature>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
List {
Button("Show Alert") {
viewStore.send(.alertButtonTapped)
}
Button("Show Confirmation") {
viewStore.send(.confirmationButtonTapped)
}
}
.alert(
store: self.store.scope(
state: \.$destination,
action: AlertsAndDialogsFeature.Action.destination
),
state: /AlertsAndDialogsFeature.Destinations.State.alert,
action: AlertsAndDialogsFeature.Destinations.Action.alert
)
.confirmationDialog(
store: self.store.scope(
state: \.$destination,
action: AlertsAndDialogsFeature.Action.destination
),
state: /AlertsAndDialogsFeature.Destinations.State.confirmation,
action: AlertsAndDialogsFeature.Destinations.Action.confirmation
)
}
}
} It seems that the custom action is nilling out the state instead of waiting for the dismiss action to do it?
|
Beta Was this translation helpful? Give feedback.
-
As soon as I use the new .ifLet(\.$destination, action: /Action.destination) {
Scope(state: /DestinationState.editFolder, action: /DestinationAction.editFolder) {
EditFolder()
}
} As a workaround you have to specify the full paths. Are there any better solutions? .ifLet(\Feature.State.$destination, action: /Feature.Action.destination) {
Scope(state: /Feature.DestinationState.editFolder, action: /Feature.DestinationAction.editFolder) {
EditFolder()
}
} |
Beta Was this translation helpful? Give feedback.
-
Hi Folks, I might have missed something from the recent episode and the new navigation tools being released but I was wondering if anyone could tell me why I am getting these purple runtime warning when I am using the new sheet(store: .. API Here is a snippet of the code I used to test the API:
|
Beta Was this translation helpful? Give feedback.
-
Is it forbidden to present another I'm trying to present alert over my sheet. but when I drag down my sheet to dismiss, my application stops. my code is like this import ComposableArchitecture
struct ParentFeature: Reducer {
@PresentationState var sheet: SheetFeature.State?
// ...
var body: some ReducerOf<Self> {
Reduce(core)
.ifLet(\.$sheet, action: /Action.sheet)
}
}
struct SheetFeature: Reducer {
@PresentationState var alert: AlertState<AlertAction>?
// ...
} |
Beta Was this translation helpful? Give feedback.
-
Is there a way to create custom ViewModifier like a PresentationSheetModifier? But PresentationSheetModifier relies on internal function and variable, so I think I can't create custom navigation tools. |
Beta Was this translation helpful? Give feedback.
-
I have been testing the
struct ChildView: View {
let store: StoreOf<ChildFeature>
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
VStack(spacing: 50) {
Button {
viewStore.send(.dismiss)
} label: {
Text("Dismiss \(viewStore.id)")
}
}.onDisappear {
viewStore.send(.onDisappear)
}
}
}
} Is this expected? |
Beta Was this translation helpful? Give feedback.
-
Is there any API (or examples) to create own custom View presentations using new navigation API? |
Beta Was this translation helpful? Give feedback.
-
I have been messing around with the navigation beta and came across an issue I cannot seem to solve. struct ExampleButton: View {
@State private var isLoading: Bool = false
var onTapped: () async -> Void
var body: some View {
Button {
if self.isLoading {
return
}
self.isLoading = true
Task {
await self.onTapped()
await MainActor.run {
self.isLoading = false
}
}
} label: {
HStack {
Text("Tap me!")
Spacer()
if isLoading {
ProgressView()
} else {
Text(">")
}
}
}
}
} It shows a ProgressView when tapped until the onTapped handler finishes.
To accomplish this I have written the following code: public struct Example: Reducer {
public struct State: Equatable {
@PresentationState public var destination: Example.State? = nil
}
public enum Action: Equatable {
case _finishNavigation
indirect case destination(PresentationAction<Example.Action>)
case startNavigation
}
@Dependency(\.suspendingClock) var clock
public init() {}
public var body: some Reducer<State, Action> {
Reduce { state, action in
struct CancelNavigationID {}
switch action {
case ._finishNavigation:
state.destination = .init()
return .none
case .destination(_):
return .none
case .startNavigation:
return .concatenate(
.cancel(id: CancelNavigationID.self),
.task {
try? await clock.sleep(for: .seconds(1))
return ._finishNavigation
}.cancellable(id: CancelNavigationID.self, cancelInFlight: true)
)
}
}
.ifLet(\.$destination, action: /Action.destination) {
Self()
}
}
}
public struct ExampleView: View {
let store: StoreOf<Example>
public init(store: StoreOf<Example>) {
self.store = store
}
public var body: some View {
List {
ExampleButton {
await ViewStore(self.store.stateless).send(.startNavigation).finish()
}
ExampleButton {
await ViewStore(self.store.stateless).send(.startNavigation).finish()
}
}
.navigationDestination(store: self.store.scope(
state: \.$destination,
action: Example.Action.destination
)) {
ExampleView(store: $0)
}
}
} This meets most of my requirements: the ProgressView gets shown when the user taps a button, when the user taps another button whilst loading the first effect gets cancelled, and when the effect gets to finish it navigates to the next view. My issue is that when the child view gets dismissed the ProgressView on the button is still showing. It disappears when dismissal finishes, so I assume that whatever long-living effect deals with navigation gets caught in ViewStore.send().finish(). Is there any way to allow the onTapped handler to finish before/during navigation without storing the button's loading state in TCA state? |
Beta Was this translation helpful? Give feedback.
-
First of all thanks for all the great work on TCA.
Any chance of fixing this issue? |
Beta Was this translation helpful? Give feedback.
-
Is it possible to support |
Beta Was this translation helpful? Give feedback.
-
Hey! First of all, great work! I am just about migrating some of my views, which works pretty well. |
Beta Was this translation helpful? Give feedback.
-
During migration, I've run into a situation which seems like it would be common place, and I'm wondering if there is a better way to approach it (perhaps through enhancements to the new navigation features). This is the flow:
I had previously implemented this by having an "Exit" button on the form, which would send a With the new navigation features, I've been able to get the same flow working, with the slight improvement that I can now use the So I guess my question is - should the "Change Confirmation" alert be the responsibility of the child reducer or the parent? If the parent, is there a way to leverage (or enhance) the new navigation features to accomplish this? I naively tried to observe the |
Beta Was this translation helpful? Give feedback.
-
Hi there. I'm experiencing some rather funky behaviour when using sheets; my sheet is being dismissed when I send actions which don't even Below are the actions sent when the button is pressed (the UI exhibits the above behaviour):
The erroneous dismissal happens immediately after the first action is received. Below is the source code for that particular action: // In VerifyEmailFeature:
case .didTapCheckVerification:
state.isCheckVerificationInProgress = true
return .run {
do {
let me = try await UserClient.shared.me()
await $0.send(.didCompleteCheckVerification(
isEmailVerified: me.isEmailVerified,
error: nil
))
} catch {
await $0.send(.didCompleteCheckVerification(
isEmailVerified: nil,
error: error.localizedDescription
))
}
}.animation()
case let .didCompleteCheckVerification(isEmailVerified, error):
state.isCheckVerificationInProgress = false
if let error {
state.alert = .error(error)
return .none
}
guard let isEmailVerified else {
return .none
}
state.isEmailVerified = isEmailVerified
guard isEmailVerified else {
return .none
}
return .run {
await $0.send(.dismiss(didCancel: false, didDeleteAccount: false))
}
// In OnboardingFeature:
case let .verifyEmail(.presented(.dismiss(didCancel, didDeleteAccount))):
state.verifyEmailState = nil
guard !didCancel && !didDeleteAccount else {
return .none
}
state.updateProfilePictureState = .init()
return .none
case .verifyEmail:
return .none |
Beta Was this translation helpful? Give feedback.
-
Hi, I have spotted something strange with
Has anyone experienced this? I am on the most up to date |
Beta Was this translation helpful? Give feedback.
-
I'm getting the dreaded "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions" error when trying to use Removing the presentation specific augmentations in the feature (@presentation, PresentationAction, etc.) and adding an
HistoryListFeature....
Also, is there a corresponding In addition, it seems like there is a gap in Apple's |
Beta Was this translation helpful? Give feedback.
-
On macOS, when I send
But actually, when I call
I guess the current implementation was done with iOS in mind. The behavior may need to be adjusted for macOS. |
Beta Was this translation helpful? Give feedback.
-
I noticed that there is no counterpart of extension View {
@ViewBuilder
public func alert<State, Action: Equatable, AlertAction>(
store: Store<PresentationState<State>, PresentationAction<Action>>,
state toDestinationState: @escaping (State) -> AlertState<AlertAction>?,
action fromDestinationAction: @escaping (AlertAction) -> Action,
dismiss: Action
) -> some View {
self.alert(
store.scope(
state: {
$0.wrappedValue
.flatMap(toDestinationState)
.flatMap {
$0.map {
$0.map(fromDestinationAction)
}
}
},
action: {
.presented($0)
}
),
dismiss: dismiss
)
}
} However, when using this helper function in following demo, an runtime warning occurs. import ComposableArchitecture
import SwiftUI
struct AlertDemo: Reducer {
struct State: Equatable {
@PresentationState var destination: Destination.State?
}
enum Action: Equatable {
case showAlert
case destination(PresentationAction<Destination.Action>)
}
struct Destination: Reducer {
enum State: Equatable {
case alert(AlertState<Action.Alert>)
}
enum Action: Equatable {
case alert(Alert)
enum Alert: Equatable {
case dismiss
case confirm
}
}
var body: some ReducerOf<Self> {
EmptyReducer()
}
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .showAlert:
state.destination = .alert(
AlertState(
title: TextState("Alert Demo"),
message: TextState("This is an Alert"),
primaryButton: .cancel(TextState("Cancel")),
secondaryButton: .destructive(
TextState("Confirm"),
action: .send(.confirm)
)
)
)
return .none
case .destination:
return .none
}
}
.ifLet(
\.$destination,
action: /Action.destination,
then: Destination.init
)
}
}
struct AlertDemoView: View {
let store: StoreOf<AlertDemo>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
Button {
viewStore.send(.showAlert)
} label: {
Text("Show Alert")
}
.alert(
store: self.store.scope(
state: \.$destination,
action: AlertDemo.Action.destination
),
state: /AlertDemo.Destination.State.alert,
action: AlertDemo.Destination.Action.alert,
dismiss: .alert(.dismiss)
)
}
}
} So when I hit the
It seems after receiving How to avoid this? |
Beta Was this translation helpful? Give feedback.
-
I am trying to follow episode 224, in particular to introduce a add item sheet to my sample app. import SwiftUI
import ComposableArchitecture
@main
struct sampleSheetsNotWorkingTCA_BikesApp: App {
var body: some Scene {
WindowGroup {
BikesView(
store: ItemsDomain.sampleStore
)
}
}
}
struct ItemsDomain: ReducerProtocol {
struct State: Equatable {
var items: IdentifiedArrayOf<Item>
@PresentationState var addItem: ItemDomain.State?
}
enum Action {
case addItem(SheetAction<ItemDomain.Action>)
}
var body: some ReducerProtocolOf<Self> {
Reduce { state, action in
switch action {
case .addItem(_):
return .none
}
}
// This throws the compiler error
// "Cannot infer key path type from context; consider explicitly specifying a root type"
.sheet(state: \.addItem, action: /Action.addItem) {
ItemDomain()
}
}
}
enum SheetAction<Action> {
case dismiss
case presented(Action)
}
extension SheetAction: Equatable where Action: Equatable {}
struct BikesView: View {
let store: StoreOf<ItemsDomain>
var body: some View {
WithViewStore(self.store, observe: \.items ) { itemsStore in
NavigationStack {
List {
ForEach(itemsStore.state) { item in
Text(item.id.uuidString)
.font(.caption)
}
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
}
//TODO: needs implementation …
// .sheet(
// store: self.store.scope(
// state: \.addItem,
// action: ItemsDomain.Action.addItem
// )
// ) { store in
// EmptyView()
// }
}
}
extension ItemsDomain {
static let sampleStore = Store(
initialState: ItemsDomain.State(
items: [
Item(
id: UUID(),
titel: "Some fancy item …"
)
]
),
reducer: ItemsDomain()
)
}
struct BikesView_Previews: PreviewProvider {
static var previews: some View {
BikesView(store: ItemsDomain.sampleStore)
}
}
struct Item: Identifiable, Equatable {
let id: UUID
var titel: String
}
struct ItemDomain: ReducerProtocol {
struct State: Equatable {
var item: Item
}
enum Action: Equatable {}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
.none
}
} |
Beta Was this translation helpful? Give feedback.
-
Thanks all of following and contributing to the discussion here! We're kicking off a new discussion for "beta 2", so please continue there: #2048 |
Beta Was this translation helpful? Give feedback.
-
Full cover sheet showed perfectly on preview but nothing showed in simulators or real device debug prints
|
Beta Was this translation helpful? Give feedback.
-
We’ve been teasing navigation tools for the Composable Architecture for a long time, and been working on the tools for even longer, but it is finally time to get a preview of what is coming to the library.
The tools being previewed today include what has been covered in our navigation series so far (episodes #222, #223, #224), as well as a few tools that will be coming in the next few episodes. In particular, this includes the tools for dealing with alerts, confirmation dialogs, sheets, popovers, fullscreen covers, pre-iOS 16 navigation links, and
navigationDestination
. Notably, this beta does not currently provide the tools for the iOS 16NavigationStack
, but that will be coming soon.All of these changes are mostly backwards compatible with the most recent version of TCA (version 0.51.0 right now), which means you can point any existing project to the beta branch to get a preview of what the tools have to offer. If you experience any compiler errors please let us know.
The basics
We aren’t going to give a detailed overview of the tools in this announcement and how we motivated and designed them (that’s what the episodes are for 😀), but most of the case studies and demos in the repo have been updated to use the new tools and there is an extensive test suite. There hasn’t been much documentation written yet, but that will be coming soon as the episode series plays out.
Here is a very quick overview of what you can look forward to:
When a parent feature needs to navigate to a child feature you will enhance its domain using the new
@PresentationState
property wrapper andPresentationAction
wrapper type:Then you will make use of the new, special
ifLet
reducer operator that can single out the presentation state and action and run the child feature on that state when it is active:That is all that is needed as far as the domain and reducer is concerned. The
ifLet
operator has been with the library since the beginning, but is now enhanced with super powers, including automatically cancelling child effects when the child is dismissed, and a lot more.There is one last thing you need to do, and that’s in the view. There are special overloads of all the SwiftUI navigation APIs (such as
.alert
,.sheet
,.popover
,.navigationDestination
etc.) that take a store instead of a binding. If you provide a store focused on presentation state and actions, it will take care of the rest. For example, if the child feature is shown in a sheet, you will do the following:And that is basically it. There’s still a lot more to the tools and things to learn, but we will leave it at that and we encourage you to explore the branch when you get a chance.
1.0 Preview
As you may have heard recently we have a 1.0 preview available to everyone who wants a peek at what APIs will be renamed and removed for the 1.0 release. Currently that branch is targeting
main
, but soon it will target thisnavigation-beta
branch, which means you can simultaneously see how to modernize your codebase for the 1.0 and check out the new navigation tools.Trying the beta
To give the beta a shot, update your SPM dependencies to point to the
navigation-beta
branch:This branch also includes updated demo applications using these APIs, so check them out if you're curious!
We really think these tools will make TCA even more fun and easier to use! If you take things for a spin, please let us know (via Twitter, Mastodon or GitHub discussions) if you have any questions, comments, concerns, or suggestions!
Beta Was this translation helpful? Give feedback.
All reactions