0

I'm learning Combine and how it can update a value using publishers. Currently I have created a variable that updates itself when validation fails.

var nameError: AnyPublisher<String, Never> {
    $name
        .dropFirst()
        .debounce(for: 0.2, scheduler: RunLoop.main)
        .removeDuplicates()
        .map {
            if $0.isEmpty || !self.isValidName() {
                return "Name not valid"
            }
            return "Name"
        }
        .eraseToAnyPublisher()
}

I want to attach this to a Text() label so that it updates. Before Combine I would have an var error: String that I would check against. But now I get the error Cannot convert value of type 'AnyPublisher<String, Never>' to expected argument type 'String'

How do I convert a var error: String message to receive an AnyPublisher<String, Never>?

Warve
  • 481
  • 1
  • 8
  • 22

1 Answers1

0

Below is a Playground that I think implements what you are asking.

The upshot is that you subscribe to your publisher in onAppear and keep the subscription in the state of your view. When a new value is published you update the state holding the caption.

import UIKit
import SwiftUI
import Combine
import PlaygroundSupport

class ModelObject : ObservableObject {
    @Published var name: String = ""
    var nameError: AnyPublisher<String, Never> {
        $name
            .dropFirst()
            .debounce(for: 0.2, scheduler: RunLoop.main)
            .print()
            .removeDuplicates()
            .map {
                if $0.isEmpty || !self.isValidName() {
                    return "Name not valid"
                }
                return ""
            }
            .eraseToAnyPublisher()
    }

    func isValidName() -> Bool {
        return name.caseInsensitiveCompare("Scott") != .orderedSame
    }
}

struct TextControl : View {
    @StateObject var viewModel = ModelObject()
    @State var caption : String = ""
    @State var subscription : AnyCancellable?

    var body : some View {
        VStack(alignment: .leading) {
            TextField("Entry", text: $viewModel.name)
            Text(caption)
                .font(.caption)
                .foregroundColor(.red)
        }
        .padding()
        .frame(width: 320, height: 240)
        .onAppear {
            self.subscription = viewModel.nameError.sink { self.caption = $0}
        }
    }
}

let viewController = UIHostingController(rootView: TextControl())
PlaygroundSupport.PlaygroundPage.current.liveView = viewController
Scott Thompson
  • 22,629
  • 4
  • 32
  • 34
  • Ok, so the onAppear links the subscriber to the nameError text. How would this work if I have 3 or 4 `TextControl` in a view, use an Subscription array? – Warve Mar 09 '22 at 09:30
  • `@State var subscriptions = Set()` then remove `self.subscription =` and chain on `... .sink { self.caption = $0}.storeIn(&subscriptions)` – Scott Thompson Mar 09 '22 at 15:50