Replies: 6 comments 7 replies
-
@mbrandonw Would love to see this added to a future version. Any idea if this is on the roadmap at all? I bet you if this were added as a proper issue, you could get someone to help out with it 😀 |
Beta Was this translation helpful? Give feedback.
-
I've tried tackling this for the last few days and have found that in order for anyone to take this task on, a few fundamental questions need to be answered. Before we go to that, let's work from what we'd ideally want, and derive the fundamentals from there. Ideally, a user would want to create an AlertState object in this fashion: let alert = AlertState {
TextState("Fact about \(count)")
} actions: {
ButtonState(role: .destructive) {
TextState("OK")
}
TextFieldState(initialText: "LOL", action: .getFact) {
TextState("Get another fact")
}
ButtonState(role: .cancel) {
TextState("LOL")
}
} message: {
TextState(fact.description)
} Based on that, we know that public struct AlertState<Action> {
public var actions: [any ActionState<Action>] = []
} But, it seems like a bad idea to now make public enum AnyActionState<Action> {
case .button(ButtonState<Action>)
case .textField(TextFieldState<Action>)
} Of course, we don't want public API to now require users wrap objects like public struct AlertState<Action> {
public var actions: [AnyActionState<Action>] = []
/* ... */
public init(
actions: [any ActionState<Action>]
) {
buttons.compactMap {
switch $0 {
case let buttonState as ButtonState<Action>:
.button(buttonState)
case let textFieldState as TextFieldState<Action>:
.textField(textFieldState)
default:
nil
}
}
}
} Unfortunately, this change now means we have to reassess what it means to map AlertState from one domain to another. This was easy before, when we had an array of one type. It does seem like adding a Instead of putting much of the focus on how
public struct TextFieldState<Action> {
// Option 1
// - Mutated outside of object
public var text: String
// Option 2
public var action: Action
public var actionCasePath: AnyCasePath<Action, String>
public var text: String {
// It's possible to use an AnyCasePath<Action, String> to populate action
// with the text:
//
// let text = textField.text ?? "" <= actual text field view
// textFieldState.action = textFieldState.actionCasePath.embed(text)
//
// However, it seems impossible to now extract this
// text from action
action.???
}
// Option 3
// - Allows us to extract action, but this becomes useless
// when trying to map TextFieldState<Action> to
// TextFieldState<NewAction> since there's no easy way
// to map AnyCasePath<Action, String> to AnyCasePath<NewAction, String>
// without using something like AnyCasePath<NewAction, Action>,
// which definitely seems promising for mapping, instead of
// using (Action) -> NewAction closure that ButtonState uses
public var actionCasePath: AnyCasePath<Action, String>
public var text: String
public var action: Action {
actionCasePath.embed(text)
}
} I'm not even bringing up how this makes conforming to
These are all the kind of questions that have made pushing a functioning PR a lot harder than I was hoping. Maybe others have some ideas and suggestions that can pave the path forward, but as of now I think I've hit a wall and can't progress further without coming up with a different way of approaching the problem. |
Beta Was this translation helpful? Give feedback.
-
While it would be great to have support for // TCA
@Reducer struct Feature {
struct State {
var isAlertPresented = false
var name = ""
// ...
}
// ...
}
struct FeatureView: View {
@Bindable var store: StoreOf<Feature>
var body: some View {
Text("Name is: \(store.name)")
.alert("Enter your name", isPresented: $store.isAlertPresented) {
TextField("Enter your name", text: $store.name)
Button("OK") { store.send(.okButtonTapped) }
}
}
}
// Vanilla SwiftUI
struct FeatureView: View {
@State var isAlertPresented = false
@State var name = ""
var body: some View {
Text("Name is: \(name)")
.alert("Enter your name", isPresented: $isAlertPresented) {
TextField("Enter your name", text: $name)
Button("OK") { /* ... */ }
}
}
} It may not be as testable as if we had full support for text fields in So, I hope no one thinks that they are being held back from SwiftUI features because of TCA. At the end of the day we want everything from SwiftUI to be available to you. |
Beta Was this translation helpful? Give feedback.
-
I'll have to try and see if the same approach can be done with UIKit. I've been using Thinking of how to go about with this did make me realize something; we are making an assumption that an alert should be able to show buttons and text fields in any order and maybe that is our problem here. I've always seen alerts show the text fields first, and then buttons after. Perhaps this means we don't need enum or protocols, and we can simply just add a new |
Beta Was this translation helpful? Give feedback.
-
To be honest, while I prefer to use the https://github.com/pointfreeco/swift-navigation/blob/main/Sources/SwiftUINavigation/Alert.swift They let you provide an optional piece of state representing the details of the alert explicitly, and provides closure hooks for using that non-optional state as actions/title/message for the alert. So, you could define something like (untested, missing protocols, etc): // TCA
@Reducer struct Feature {
@Reducer struct Authenticate {
struct State: Equatable {
var username: String
var password: String
var errorMessage: String?
}
public enum Action {
case logInButtonTapped(String username, String password)
case cancelButtonTapped
}
// ..
}
struct State {
var authenticate: Authenticate?
// ...
}
// ...
}
struct FeatureView: View {
@Bindable var store: StoreOf<Feature>
var body: some View {
Text("Name is: \(store.name)")
.alert(item: $store.authenticate) { _ in
"Please authenticate."
} actions: { authStore in
TextField("Username", text: $authStore.username)
SecureField("Password", text: $authStore.password)
Button("Log In") { authStore.send(.logInButtonTapped(authStore.username, authStore.password)) }
Button("Cancel") { authStore.send(.cancelButtonTapped) }
} message: {
$0.errorMessage ?? "Enter your details."
}
}
} |
Beta Was this translation helpful? Give feedback.
-
After messing around with this more in UIKit, I have come to appreciate what you all are suggesting about using some extra state in the view domain to support adding text fields in alerts. It turns out that One other approach (for UIKit developers) could be adding a Going with that approach, I can easily create a class MyViewController: UIViewController {
public func viewDidLoad() {
super.viewDidLoad()
present(
item: $store.scope(
state: \.destination?.alert,
action: \.destination.alert
)
) { [weak store] alertStore in
let alertState = alertStore.withState({ $0 })
let alertController = UIAlertController(store: alertStore)
with(alertController, Styling.uiAlertController.A11y.configure)
switch alertState {
case .updateLoopName:
guard let loopName = store?.mediaPlayer.loop?.name
else { return alertController }
let loopTitleBinding = UIBinding(wrappedValue: loopName)
alertController.addTextField {
$0.placeholder = "Loop title"
$0.bind(text: loopTitleBinding)
}
let saveButtonState = ButtonState<Home.Destination.Alert> {
TextState("Save")
}
let saveAction = UIAlertAction(
saveButtonState,
action: { [weak alertStore] _ in
let loopTitle = loopTitleBinding.wrappedValue
alertStore?.send(.saveLoop(.updateLoopName(loopTitle)))
}
)
alertController.addAction(saveAction)
default:
break
}
return alertController
}
}
} This approach is great too because the Thank you everyone for dealing with my spam and providing your insight! I have once again managed to dodge for another day having to learn the perfect, definitely-ready-for-production, get-all-the-attention SwiftUI framework 😜 |
Beta Was this translation helpful? Give feedback.
-
Hi,
is it possible to create an AlertState with state for a TextField and/or a SecureField?
Thx
@trispo
Beta Was this translation helpful? Give feedback.
All reactions