Publishing and Consuming a transcript from SFSpeechRecognizer - swift

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

Related

Saving a list using Codable or userDefaults

Can someone help me to save the list in this code using Codable or another methods. I am not able to use the UserDefaults in the code. Can anyone help me how to use save the lists so that when ever, I re-open my app, the list is still there. Thanks.
import SwiftUI
struct MainView: View {
#State var br = Double()
#State var loadpay = Double()
#State var gp : Double = 0
#State var count: Int = 1
#State var listcheck = Bool()
#StateObject var taskStore = TaskStore()
#State var name = String()
var userCasual = UserDefaults.standard.value(forKey: "userCasual") as? String ?? ""
func addNewToDo() {
taskStore.tasks.append(Task(id: String(taskStore.tasks.count + 1), toDoItem: "load \(count)", amount: Double(gp)))
}
func stepcount() {
count += 1
}
var body: some View {
VStack {
TextField("Name", text: $name)
HStack {
Button(action: { gp += loadpay }) {
Text("Add Load")
}
Button(action: {
addNewToDo()
}) {
Text("Check")
}
}
Form {
ForEach(self.taskStore.tasks) {
task in
Text(task.toDoItem)
}
}
}
Button(action: {
UserDefaults.standard.set(name, forKey: "userCasual")})
{Text("Save")}
}
}
struct Task : Identifiable {
var id = String()
var toDoItem = String()
var amount : Double = 0
}
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
}
In Task adopt Codable
struct Task : Codable, Identifiable {
var id = ""
var toDoItem = ""
var amount = 0.0
}
In TaskStore add two methods to load and save the tasks and an init method
class TaskStore : ObservableObject {
#Published var tasks = [Task]()
init() {
load()
}
func load() {
guard let data = UserDefaults.standard.data(forKey: "tasks"),
let savedTasks = try? JSONDecoder().decode([Task].self, from: data) else { tasks = []; return }
tasks = savedTasks
}
func save() {
do {
let data = try JSONEncoder().encode(tasks)
UserDefaults.standard.set(data, forKey: "tasks")
} catch {
print(error)
}
}
}
In the view call taskStore.save() to save the data.
However: For large data sets UserDefaults is the wrong place. Save the data in the Documents folder or use Core Data.
Side note: Never use value(forKey:) in UserDefaults, in your example there is string(forKey:)
You should take a look at the #AppStorage property wrapper. Here is a great article written by Paul Hudson who is a great resource when you're learning iOS.
UserDefaults isn't the best way to store persistent information though. Once you get a bit more comfortable with Swift and SwiftUI, you should look into CoreData for storing your data across sessions.

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.

#ObservedObject not updating after successful network call

I've hit a brick wall in my widget extension. I'm using AlamoFire and ObjectMapper to match the networking we have in the main app. I can tell that my AlamoFire network call is getting triggered and that I'm getting results back, and in the correct, expected format. However, saving the response of that network call to a #Published var doesn't seem to be working. My view and models/structs are below:
struct WidgetEntryView: View {
var entry: ResourceCategoryEntry
#ObservedObject var viewModel = WidgetResourcesView(widgetSize: .medium)
var body: some View {
if UserDefaults.forAppGroup.object(forKey: "sessionToken") as? String == nil {
PleaseLogIn()
} else if viewModel.mediumResources.count < 1 {
ErrorScreen()
} else {
MediumResourcesView(resources: viewModel.mediumResources)
}
}
}
class WidgetResourcesView: ObservableObject {
#Published var resourceGroups: [WidgetResouceGroup] = [WidgetResouceGroup]()
var widgetSize: WidgetSize = .small
var selectedCategory: String?
init(widgetSize: WidgetSize) {
self.widgetSize = widgetSize
self.selectedCategory = UserDefaults.forAppGroup.string(forKey: ResourceCategoryEntry.userDefaultKey)
getResources()
}
func getResources() {
WidgetNetworkService.getResources(widgetSize: self.widgetSize.rawValue, selectedCategory: self.selectedCategory) { resourceGroups in
DispatchQueue.main.async {
self.resourceGroups = resourceGroups
}
} failure: { _ in
print("Error Received")
}
}
var mediumResources: [WidgetResource] {
var resources = [WidgetResource]()
if let featuredResourceGroup = resourceGroups.featuredResourceGroup {
for resource in featuredResourceGroup.resources { resources.append(resource) }
}
if let nonFeaturedResourceGroup = resourceGroups.nonFeaturedResourceGroup {
for resource in nonFeaturedResourceGroup.resources { resources.append(resource) }
}
return resources
}
}
class WidgetResouceGroup: NSObject, Mappable, Identifiable {
var id = UUID()
var widgetCategory: WidgetCategory = .featured
var resources = [WidgetResource]()
required init?(map: Map) {}
func mapping(map: Map) {
id <- map["section"]
widgetCategory <- map["section"]
resources <- map["resources"]
}
}
typealias WidgetResourceGroupCollection = [WidgetResouceGroup]
extension WidgetResourceGroupCollection {
var featuredResourceGroup: WidgetResouceGroup? {
return first(where: {$0.widgetCategory == .featured})
}
var nonFeaturedResourceGroup: WidgetResouceGroup? {
return first(where: {$0.widgetCategory != .featured})
}
}
class WidgetResource: NSObject, Mappable, Identifiable {
enum ResourceType: String {
case text = "text"
case audio = "audio"
case video = "video"
}
var id = 0
var title = ""
var imageInfo: WidgetImageInfo?
var resourceType: ResourceType = .text
required init?(map: Map) {}
func mapping(map: Map) {
id <- map["object_id"]
title <- map["title"]
imageInfo <- map["image_info"]
resourceType <- map["content_type"]
}
}
You can use the objectWillChange - Property in your observable object to specifiy when the observable object should be refreshed.
Apple Dev Doku
Example by Paul Hudson
WidgetEntryView instantiates WidgetResourcesView using the ObservedObject wrapper. This causes a new instance of WidgetResourcesView to be instantiated again on every refresh. Try switching that to StateObject, and the original object will be kept in memory between view updates. I believe this is the only change needed, but I’m away so can’t test it!

How to associate a block of code with a #State var when it changes

Whenever the value of the #State variable myData changes I would like to be notified and store that data in an #AppStorage variable myStoredData. However, I don't want to have to write explicitly this storing code everywhere the state var is changed, I would like to associate a block of code with it that gets notified whenever the state var changes and performs storage. The reason for this is, for example, I want to pass the state var as a binding to another view, and when that view would change the variable, the storage block would automatically be executed. How can I do this/can I do this in SwiftUI?
struct MyView : View {
#AppStorage("my-data") var myStoredData : Data!
#State var myData : [String] = ["hello","world"]
var body : some View {
Button(action: {
myData = ["something","else"]
myStoredData = try? JSONEncoder().encode(myData)
}) {
Text("Store data when button pressed")
}
.onAppear {
myData = (try? JSONDecoder().decode([String].self, from: myStoredData)) ?? []
}
}
}
I'm looking for something like this, but this does not work:
#State var myData : [String] = ["hello","world"] {
didSet {
myStoredData = try? JSONEncoder().encode(myData)
}
}
Simply use .onChange modifier anywhere in view (like you did with .onAppear)
struct MyView : View {
#AppStorage("my-data") var myStoredData : Data!
#State var myData : [String] = ["hello","world"]
var body : some View {
Button(action: {
myData = ["something","else"]
myStoredData = try? JSONEncoder().encode(myData)
}) {
Text("Store data when button pressed")
}
.onChange(of: myData) {
myStoredData = try? JSONEncoder().encode($0) // << here !!
}
.onAppear {
myData = (try? JSONDecoder().decode([String].self, from: myStoredData)) ?? []
}
}
}
You can add a set callback extension to the binding to monitor the value change
extension Binding {
/// When the `Binding`'s `wrappedValue` changes, the given closure is executed.
/// - Parameter closure: Chunk of code to execute whenever the value changes.
/// - Returns: New `Binding`.
func onChange(_ closure: #escaping () -> Void) -> Binding<Value> {
Binding(get: {
wrappedValue
}, set: { newValue in
wrappedValue = newValue
closure()
})
}
}
Use extension code
$myData.onChange({
})

Using Combine to parse phone number String

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.