Using Combine to parse phone number String - swift

I'm trying to wrap my mind around how Combine works. I believe I'm doing something wrong when I use the .assign operator to mutate the #Published property I'm operating on. I've read the documentation on Publishers, Subscribers, and Operators. But I'm a bit loose on where exactly to create the Publisher if I don't want it to be a function call.
import SwiftUI
import Combine
struct PhoneNumberField: View {
let title: String
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField(title,text: $viewModel.text)
}
class ViewModel: ObservableObject {
#Published var text: String = ""
private var disposables = Set<AnyCancellable>()
init() {
$text.map { value -> String in
self.formattedNumber(number: value)
}
//something wrong here
.assign(to: \.text, on: self)
.store(in: &disposables)
}
func formattedNumber(number: String) -> String {
let cleanPhoneNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let mask = "+X (XXX) XXX-XXXX"
var result = ""
var index = cleanPhoneNumber.startIndex
for ch in mask where index < cleanPhoneNumber.endIndex {
if ch == "X" {
result.append(cleanPhoneNumber[index])
index = cleanPhoneNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
}
}
struct PhoneNumberParser_Previews: PreviewProvider {
static var previews: some View {
PhoneNumberField(title: "Phone Number")
}
}

Use .receive(on:):
$text.map { self.formattedNumber(number: $0) }
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.text = value
})
.store(in: &disposables)
This will allow you to listen to changes of the text variable and update it in the main queue. Using main queue is necessary if you want to update #Published variables read by some View.
And to avoid having a retain cycle (self -> disposables -> assign -> self) use sink with a weak self.

Related

Publishing and Consuming a transcript from SFSpeechRecognizer

I'm using Apple's example of an Observable wrapper around SFSpeechRecognizer as follows:
class SpeechRecognizer: ObservableObject {
#Published var transcript: String
func transcribe() {}
}
The goal is to use a ViewModel to both consume the transcript as it is generated, as well as passing on the value to a SwiftUI View for visual debugging:
class ViewModel : ObservableObject {
#Published var SpeechText: String = ""
#ObservedObject var speech: SpeechRecognizer = SpeechRecognizer()
public init() {
speech.transcribe()
speech.transcript.publisher
.map { $0 as! String? ?? "" }
.sink(receiveCompletion: {
print ($0) },
receiveValue: {
self.SpeechText = $0
self.doStuff(transcript: $0)
})
}
private void doStuffWithText(transcript: String) {
//Process the output as commands in the application
}
}
I can confirm that if I observe transcript directly in a SwiftUI view, that the data is flowing through. My problem is receiving the values as they change, and then assigning that data to my own published variable.
How do I make this work?
Subscription should be stored otherwise it is canceled immediately, also you need to make subscription before actual usage (and some other memory related modifications made). So I assume you wanted something like:
class ViewModel : ObservableObject {
#Published var SpeechText: String = ""
var speech: SpeechRecognizer = SpeechRecognizer() // << here !!
private var subscription: AnyCancellable? = nil // << here !!
public init() {
self.subscription = speech.transcript.publisher // << here !!
.map { $0 as! String? ?? "" }
.sink(receiveCompletion: {
print ($0) },
receiveValue: { [weak self] value in
self?.SpeechText = value
self?.doStuffWithText(transcript: value)
})
self.speech.transcribe() // << here !!
}
private func doStuffWithText(transcript: String) {
//Process the output as commands in the application
}
}
Tested with Xcode 13.2

How to pass a parent ViewModel #Published property to a child ViewModel #Binding using MVVM

I'm using an approach similar to the one described on mockacoding - Dependency Injection in SwiftUI where my main ViewModel has the responsibility to create child viewModels.
In the code below I am not including the Factory, as it's very similar to the contents of the post above: it creates the ParentViewModel, passes to it dependencies and closures that construct the child view models.
struct Book { ... } // It's a struct, not a class
struct ParentView: View {
#StateObject var viewModel: ParentViewModel
var body: some View {
VStack {
if viewModel.book.bookmarked {
BookmarkedView(viewModel: viewModel.makeBookMarkedViewModel())
} else {
RegularView(viewModel: viewModel.makeBookMarkedViewModel())
}
}
}
}
class ParentViewModel: ObservableObject {
#Published var book: Book
// THIS HERE - This is how I am passing the #Published to #Binding
// Problem is I don't know if this is correct.
//
// Before, I was not using #Binding at all. All where #Published
// and I just pass the reference. But doing that would cause for
// the UI to NEVER update. That's why I changed it to use #Binding
private var boundBook: Binding<Book> {
Binding(get: { self.book }, set: { self.book = $0 })
}
// The Factory object passes down these closures
private let createBookmarkedVM: (_ book: Binding<Book>) -> BookmarkedViewModel
private let createRegularVM: (_ book: Binding<Book>) -> RegularViewModel
init(...) {...}
func makeBookmarkedViewModel() {
createBookmarkedVM(boundBook)
}
}
class BookmarkedView: View {
#StateObject var viewModel: BookmarkedViewModel
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Text(book.title) // <---- THIS IS THE PROBLEM. Not being updated
Button("Remove bookmark") {
viewModel.removeBookmark()
}
}
.onReceive(timer) { _ in
print("adding letter") // <-- this gets called
withAnimation {
viewModel.addLetterToBookTitle()
}
}
}
}
class BookmarkedViewModel: ObservableObject {
#Binding var book: Book
// ... some other dependencies passed by the Factory object
init(...) { ... }
public func removeBookmark() {
// I know a class would be better than a struct, bear with me
book = Book(title: book.title, bookmarked: false)
}
/// Adds an "a" to the title
public func addLetterToBookTitle() {
book = Book(title: book.title + "a", bookmarked: book.bookmarked)
print("letter added") // <-- this gets called as well
}
}
From the code above, let's take a look at BookmarkedView. If I click the button and viewModel.removeBookmark() gets called, the struct is re-assigned and ParentView now renders RegularView.
This tells me that I successfully bound #Published book: Book from ParentViewModel to #Binding book: Book from BookmarkedViewModel, through its boundBook computed property. This felt like the most weird thing I had to make.
However, the problem is that even though addLetterToBookTitle() is also re-assigning the book with a new title, and it should update the Text(book.title), it's not happening. The same title is being displayed.
I can guarantee that the book title has change (because of some other components of the app I'm omitting for simplicity), but the title's visual is not being updated.
This is the first time I'm trying out these pattern of having a view model build child view models, so I appreciate I may be missing something fundamental. What am I missing?
EDIT:
I made an MVP example here: https://github.com/christopher-francisco/TestMVVM/tree/main/MVVMTest.xcodeproj
I'm looking for whether:
My take at child viewmodels is fundamentally wrong and I should start from scratch, or
I have misunderstood #Binding and #Published attributes, or
Anything really
Like I said initially #Binding does not work in a class you have to use .sink to see the changes to an ObservableObject.
See below...
class MainViewModel: ObservableObject {
#Published var timer = YourTimer()
let store: Store
let nManager: NotificationManager
let wManager: WatchConnectionManager
private let makeNotYetStartedViewModelClosure: (_ parentVM: MainViewModel) -> NotYetStartedViewModel
private let makeStartedViewModelClosure: (_ parentVM: MainViewModel) -> StartedViewModel
init(
store: Store,
nManager: NotificationManager,
wManager: WatchConnectionManager,
makeNotYetStartedViewModel: #escaping (_ patentVM: MainViewModel) -> NotYetStartedViewModel,
makeStartedViewModel: #escaping (_ patentVM: MainViewModel) -> StartedViewModel
) {
self.store = store
self.nManager = nManager
self.wManager = wManager
self.makeNotYetStartedViewModelClosure = makeNotYetStartedViewModel
self.makeStartedViewModelClosure = makeStartedViewModel
}
}
// MARK: - child View Models
extension MainViewModel {
func makeNotYetStartedViewModel() -> NotYetStartedViewModel {
self.makeNotYetStartedViewModelClosure(self)
}
func makeStartedViewModel() -> StartedViewModel {
self.makeStartedViewModelClosure(self)
}
}
class NotYetStartedViewModel: ObservableObject {
var parentVM: MainViewModel
var timer: YourTimer{
get{
parentVM.timer
}
set{
parentVM.timer = newValue
}
}
var cancellable: AnyCancellable? = nil
init(parentVM: MainViewModel) {
self.parentVM = parentVM
//Subscribe to the parent
cancellable = parentVM.objectWillChange.sink(receiveValue: { [self] _ in
//Trigger reload
objectWillChange.send()
})
}
func start() {
// I'll make this into a class later on
timer = YourTimer(remainingSeconds: timer.remainingSeconds, started: true)
}
}
class StartedViewModel: ObservableObject {
var parentVM: MainViewModel
var timer: YourTimer{
get{
parentVM.timer
}
set{
parentVM.timer = newValue
}
}
var cancellable: AnyCancellable? = nil
init(parentVM: MainViewModel) {
self.parentVM = parentVM
cancellable = parentVM.objectWillChange.sink(receiveValue: { [self] _ in
//trigger reload
objectWillChange.send()
})
}
func tick() {
// I'll make this into a class later on
timer = YourTimer(remainingSeconds: timer.remainingSeconds - 1, started: timer.started)
}
func cancel() {
timer = YourTimer()
}
}
But this is an overcomplicated setup, stick to class or struct. Also, maintain a single source of truth. That is basically the center of how SwiftUI works everything should be getting its value from a single source.

SwiftUI+Combine - Dynamicaly subscribing to a dict of publishers

In my project i hold a large dict of items that are updated via grpc stream. Inside the app there are several places i am rendering these items to UI and i would like to propagate the realtime updates.
Simplified code:
struct Item: Identifiable {
var id:String = UUID().uuidString
var name:String
var someKey:String
init(name:String){
self.name=name
}
}
class DataRepository {
public var serverSymbols: [String: CurrentValueSubject<Item, Never>] = [:]
// method that populates the dict
func getServerSymbols(serverID:Int){
someService.fetchServerSymbols(serverID: serverID){ response in
response.data.forEach { (name,sym) in
self.serverSymbols[name] = CurrentValueSubject(Item(sym))
}
}
}
// background stream that updates the values
func serverStream(symbols:[String] = []){
someService.initStream(){ update in
DispatchQueue.main.async {
self.serverSymbols[data.id]?.value.someKey = data.someKey
}
}
}
}
ViewModel:
class SampleViewModel: ObservableObject {
#Injected var repo:DataRepository // injection via Resolver
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
func getItem(item:String) -> Item {
guard let item = repo.serverSymbols[item] else { return Item(name:"N/A")}
return ItemPublisher(item: item).data
}
}
class ItemPublisher: ObservableObject {
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:CurrentValueSubject<Item, Never>){
item
.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
Main View with subviews:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item: viewModel.getItem(item: item))
}
}
}
}
struct FavoriteCardView: View {
var item:Item
var body: some View {
VStack {
Text(item.name)
Text(item.someKey) // dynamic value that should receive the updates
}
}
}
I must've clearly missed something or it's a completely wrong approach, however my Item cards do not receive any updates (i verified the backend stream is active and serverSymbols dict is getting updated). Any advice would be appreciated!
I've realised i've made a mistake - in order to receive the updates i need to pass down the ItemPublisher itself. (i was incorrectly returning ItemPublisher.data from my viewModel's method)
I've refactored the code and make the ItemPublisher provide the data directly from my repository using the item key, so now each card is subscribing individualy using the publisher.
Final working code now:
class SampleViewModel: ObservableObject {
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
}
MainView and CardView:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item)
}
}
}
}
struct FavoriteCardView: View {
var itemName:String
#ObservedObject var item:ItemPublisher
init(_ itemName:String){
self.itemName = itemName
self.item = ItemPublisher(item:item)
}
var body: some View {
let itemData = item.data
VStack {
Text(itemData.name)
Text(itemData.someKey)
}
}
}
and lastly, modified ItemPublisher:
class ItemPublisher: ObservableObject {
#Injected var repo:DataRepository
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:String){
self.data = Item(name:item)
if let item = repo.serverSymbols[item] {
self.data = item.value
item.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
}

Combine #Published property send values also when not updated

I was trying to create a dynamic Form using SwiftUI and Combine, that loads options of an input (in the example, number) based on another input (in the example, myString).
The problem is that the Combine stack get executed continuously, making lots of network requests (in the example, simulated by the delay), even if the value is never changed.
I think that the expected behavior is that $myString publishes values only when it changes.
class MyModel: ObservableObject {
// My first choice on the form
#Published var myString: String = "Jhon"
// My choice that depends on myString
#Published var number: Int?
var updatedImagesPublisher: AnyPublisher<Int, Never> {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
struct ContentView: View {
#ObservedObject var model: MyModel = MyModel()
var body: some View {
Text("\(model.number ?? -100)")
.onReceive(model.updatedImagesPublisher) { newNumber in
self.model.number = newNumber
}
}
}
The problem is the updatedImagesPublisher is a computed property. It means that you create a new instance every time you access it. What happens in your code. The Text object subscribes to updatedImagesPublisher, when it receives a new value, it updates the number property of the Model. number is #Published property, it means that objectWillChange method will be called every time you change it and the body will be recreated. New Text will subscribe to new updatedImagesPublisher (because it is computed property) and receive the value again. To avoid such behaviour just use lazy property instead of computed property.
lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()
I assume it is because you create new publisher for every view update, try the following instead. (Tested with Xcode 11.4)
class MyModel: ObservableObject {
// My first choice on the form
#Published var myString: String = "Jhon"
// My choice that depends on myString
#Published var number: Int?
lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()
}

Convert a #State into a Publisher

I want to use a #State variable both for the UI and for computing a value.
For example, let's say I have a TextField bound to #State var userInputURL: String = "https://". How would I take that userInputURL and connect it to a publisher so I can map it into a URL.
Pseudo code:
$userInputURL.publisher()
.compactMap({ URL(string: $0) })
.flatMap({ URLSession(configuration: .ephemeral).dataTaskPublisher(for: $0).assertNoFailure() })
.eraseToAnyPublisher()
You can't convert #state to publisher, but you can use ObservableObject instead.
import SwiftUI
final class SearchStore: ObservableObject {
#Published var query: String = ""
func fetch() {
$query
.map { URL(string: $0) }
.flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
.sink { print($0) }
}
}
struct ContentView: View {
#StateObject var store = SearchStore()
var body: some View {
VStack {
TextField("type something...", text: $store.query)
Button("search") {
self.store.fetch()
}
}
}
}
You can also use onChange(of:) to respond to #State changes.
struct MyView: View {
#State var userInputURL: String = "https://"
var body: some View {
VStack {
TextField("search here", text: $userInputURL)
}
.onChange(of: userInputURL) { _ in
self.fetch()
}
}
func fetch() {
print("changed", userInputURL)
// ...
}
}
Output:
changed https://t
changed https://ts
changed https://tsr
changed https://tsrs
changed https://tsrst
The latest beta has changed how variables are published so I don't think that you even want to try. Making ObservableObject classes is pretty easy but you then want to add a publisher for your own use:
class ObservableString: Combine.ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<String, Never>()
var string: String {
willSet { objectWillChange.send() }
didSet { publisher.send(string) }
}
init(_ string: String = "") { self.string = string }
}
Instead of #State variables you use #ObservableObject and remember to access the property string directly rather than use the magic that #State uses.
After iOS 14.0, you can access to Publisher.
struct MyView: View {
#State var text: String?
var body: some View {
Text(text ?? "")
.onReceive($text.wrappedValue.publisher) { _ in
let publisher1: Optional<String>.Publisher = $text.wrappedValue.publisher
// ... or
let publisher2: Optional<String>.Publisher = _text.wrappedValue.publisher
}
}
}