TCA driving UIViewRepresentable
/NSViewRepresentable
#499
-
Hello! Has anyone played around with driving I will concede up front I am inexperienced with UIKit and AppKit. I looked at the UIKit case studies in the TCA repository, but this For example, I went to wrap struct ComboBoxRepresentableState: Equatable {
let options: [String]?
var selection: String
}
enum ComboBoxRepresentableAction {
case selectionDidChange(Notification)
case selectionIsChanging(Notification)
case willDismiss(Notification)
case willPopUp(Notification)
}
let comboBoxRepresentableReducer = Reducer<ComboBoxRepresentableState, ComboBoxRepresentableAction, Void> { state, action, _ in
switch action {
case let .selectionDidChange(notification):
guard let comboBox = notification.object as? NSComboBox,
let value = comboBox.objectValueOfSelectedItem as? String else { return .none }
state.selection = value
return .none
case .selectionIsChanging, .willDismiss, .willPopUp:
return .none
}
}
struct ComboBoxRepresentable: NSViewRepresentable {
let store: Store<ComboBoxRepresentableState, ComboBoxRepresentableAction>
@ObservedObject private var viewStore: ViewStore<ComboBoxRepresentableState, ComboBoxRepresentableAction>
init(store: Store<ComboBoxRepresentableState, ComboBoxRepresentableAction>) {
self.store = store
self.viewStore = ViewStore(store)
}
final class Coordinator: NSObject, NSComboBoxDelegate {
@ObservedObject private var viewStore: ViewStore<ComboBoxRepresentableState, ComboBoxRepresentableAction>
init(_ viewStore: ViewStore<ComboBoxRepresentableState, ComboBoxRepresentableAction>) {
self.viewStore = viewStore
}
func comboBoxSelectionDidChange(_ notification: Notification) {
viewStore.send(.selectionDidChange(notification))
}
}
func makeCoordinator() -> Coordinator {
Coordinator(viewStore)
}
func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
if let options = viewStore.options {
comboBox.addItems(withObjectValues: options)
}
return comboBox
}
func updateNSView(_ nsView: NSComboBox, context: Context) {
nsView.stringValue = viewStore.selection
}
} Setting aside that this implementation is not complete because it does not propagate But second, if this is a reasonable thing to do, my Spidey-sense feels like there's some API improvements to be had here, but I'm not quite able to figure them out. E.g., it almost seems like a Thank you for any thoughts. Have a nice day 🙂. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
About your issue with the Store not updating, it is very easy to forget to pullback the combo reducer into its parent, as it will compile and will most likely not produce errors at runtime. You also don't need to use When I'm using For In other words, I would do something like this (not tested, but I think you'll get the idea) struct MyView: View {
@ObservedObject var viewStore: ViewStore<State, Action>
var body: some View {
ComboBoxRepresentable(options: viewStore.options,
selection: viewStore.binding(get: \State.selection,
set: Action.selection))
}
}
struct ComboBoxRepresentable: NSViewRepresentable {
let options: [String]
var selection: Binding<String>
init(options: [String], selection: Binding<String>) {
self.options = options
self.selection = selection
}
final class Coordinator: NSObject, NSComboBoxDelegate, NSComboBoxDataSource {
let selection: Binding<String>
init(selection: Binding<String>) {
self.selection = selection
}
func comboBoxSelectionDidChange(_ notification: Notification) {
let combo = notification.object as! NSComboBox
selection.wrappedValue = combo.stringValue
}
}
func makeCoordinator() -> Coordinator {
Coordinator(selection: selection)
}
func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.delegate = context.coordinator
comboBox.addItems(withObjectValues: options)
return comboBox
}
func updateNSView(_ nsView: NSComboBox, context: Context) {
nsView.stringValue = selection.wrappedValue
}
} Of course, the coordinator can handle more delegate methods, or even be the dataSource. I personally like to put these very general struct at the same level as other SwiftUI controls, as they'll behave the same when used together. Again, this is my personal take on the matter and I'm not bearing any universal truth. Using TCA to achieve this seems absolutely doable, but I'm afraid you'll introduce a lot of boilerplate for very little returns. You can also write the vanilla wrapper, and then wrap it itself in a pure SwiftUI/TCA view, but here again, I don't know if it will be easier to use than a vanilla-like SwiftUI control. |
Beta Was this translation helpful? Give feedback.
-
Yeah, I agree with @RabugenTom. Anytime I've had to deal with view representables I've kept them as a plain SwiftUI entity using bindings and then passed in TCA derived bindings from the outside. This may not work with really complex view representables, but it's a good idea to start there. We have an example of this in the ComposableCoreLocation repo where we have a And then we pass in a view store binding here: |
Beta Was this translation helpful? Give feedback.
About your issue with the Store not updating, it is very easy to forget to pullback the combo reducer into its parent, as it will compile and will most likely not produce errors at runtime. You also don't need to use
@ObservedObject
in yourCoodinator
as it is not aView
(it shouldn't cause any issue though). I would also check thatstate.selection = value
after theguard
in the reducer is hit.When I'm using
UI/NSViewRepresentable
, I totally forget that I'll use them with TCA later, and I try to make them behave as vanilla SwiftUI views.For
NSComboBox
, I would personally use a binding (you can even use generic values if you want), and use theCoordinator
as a delegate like you did.In o…