It's not clear to me how you would combine the output of the following computed property, to the UI.
var isComplete: Bool {
Set([.givenName, .familyName]).isSubset(of: elements)
}
I essentially want the user interface to update if the above changes. How would I do this using Combine?
Reactive programming demands that I now think backwards and I'm having trouble thinking about model <<< UI rather than model >>> UI.
Here is the code in context.
struct EditPersonView: View {
let model: ViewModel
private var captionView: some View {
HStack {
/*
stuff
*/
if submitted && model.name.isComplete {
Spacer()
Text("select".localizedCapitalized) + Text(" ") + Text("save") + Text(" ") + Text("👆")
}
}
}
var body: some View {
/*
stuff - including captionView
*/
}
}
extension EditPersonView {
final class ViewModel {
let name: PersonName
init(person: Person) {
self.name = PersonName(for: person)
}
}
}
extension EditPersonView.ViewModel {
final class PersonName {
let person: Person
private let formatter = PersonNameComponentsFormatter()
init(for person: Person) {
self.person = person
}
var text: String {
get { person.name ?? "" }
set { person.name = newValue }
}
private var components: PersonNameComponents? {
formatter.personNameComponents(from: text)
}
var givenName: String? {
components?.givenName
}
var familyName: String? {
components?.familyName
}
private func isValid(component: String?) -> Bool {
if let name = component, name.count > 1 {
return true
}
return false
}
var elements: Set<Elements> {
var collection = Set<Elements>()
if isValid(component: givenName) { collection.insert(.givenName) }
if isValid(component: familyName) { collection.insert(.familyName) }
return collection
}
var isComplete: Bool {
Set([.givenName, .familyName]).isSubset(of: elements)
}
}
}
extension EditPersonView.ViewModel.PersonName {
enum Elements {
case givenName, familyName
}
}
Below is what I came up with.
The root of the sequence is the textPublisher
. This begins the sequence with the values sent to text
.
didSet
sends the text to the sequence and saves it in the person's name just as the original code does.
isComplete
becomes a publisher that sends true
or false
depending on whether the components are valid. The chain of map
operators each take the value through one step of the computations in your original code. You could easily reduce this to a single map
I would think. (or filter the computations out into functions with meaningful names and substitute the functions for the closures)
An external Subscriber
could subscribe to isComplete
and respond when it emits a true
value.
final class PersonName {
var person: Person
private let formatter = PersonNameComponentsFormatter()
let textPublisher = PassthroughSubject<String, Never>()
var text: String {
get { person.name ?? "" }
set { textPublisher.send(newValue); person.name = newValue }
}
var isComplete : AnyPublisher<Bool, Never>!
init(for person: Person) {
self.person = person
isComplete = textPublisher
.map{ self.formatter.personNameComponents(from: $0) }
.map{ (components: PersonNameComponents?) -> Set<Elements> in
var collection = Set<Elements>()
if let components = components {
if self.isValid(component: components.givenName) { collection.insert(.givenName) }
if self.isValid(component: components.familyName) { collection.insert(.familyName) }
}
return collection
}
.map { Set([Elements.givenName, Elements.familyName]).isSubset(of: $0) }
.eraseToAnyPublisher()
}
private func isValid(component: String?) -> Bool {
if let name = component, name.count > 1 {
return true
}
return false
}
}