AVSpeechSynthesizerDelegate implementation in SwiftUI - swiftui

Can anyone share how we can implement AVSpeechSynthesizerDelegate in SwiftUI.
how we can listen to delegate callbacks methods in SwiftUI app.
Thanks

One solution would be to define a class which conforms to ObservableObject. The idea would be to use an #Published property to enable SwiftUI to make updates to your UI. Here's an example of a simple way to keep track of the state of an AVSpeechSynthesizer (I'm unsure of your actual use case):
final class Speaker: NSObject, ObservableObject {
#Published var state: State = .inactive
enum State: String {
case inactive, speaking, paused
}
override init() {
super.init()
synth.delegate = self
}
func speak(words: String) {
synth.speak(.init(string: words))
}
private let synth: AVSpeechSynthesizer = .init()
}
Then, make this class conform to AVSpeechSynthesizerDelegate like so:
extension Speaker: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
self.state = .speaking
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
self.state = .paused
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
self.state = .inactive
}
// etc...
}
Here, I've simply used the delegate callbacks to update a single #Published property, but you could update however you like here depending on your use case. The main point to bear in mind with ObservableObjects is using the #Published property wrapper for any properties you wish to drive UI updates upon a change in value. Here's an example view:
struct MyView: View {
#ObservedObject var speaker: Speaker
var body: some View {
// 1
Text("State = \(speaker.state.rawValue)")
.onReceive(speaker.$state) { state in
// 2
}
}
}
Note how there's two ways to use #Published properties in SwiftUI Views. 1: Simply read the value. SwiftUI will update your view upon a value change. 2: Access the #Published property's publisher with the $ prefix. Using Views onReceive method, you can execute code whenever the publisher emits a value.

Related

Unit testing SwiftUI/Combine #Published boolean values

I am trying to acquaint myself with unit testing some view models in SwiftUI. The view model currently has two #Published boolean values that publish changes when an underlying UserDefaults property changes. For my unit tests, I have followed this guide on how to setup UserDefaults for testing so my production values are not modified. I am able to test the default value as such:
func testDefaultValue() {
XCTAssertFalse(viewModel.canDoThing)
}
How would I go about toggling the #Published value then ensuring my view model has received the changes? So for instance, I have a reference to my mock user defaults in my XCTestCase. I attempted to do the following with zero success:
func testValueTogglesToTrue() {
defaults.canDoThing = true
XCTAssertTrue(viewModel.canDoThing)
}
The thought being that updating the underlying user defaults value that is publishing changes to the published value in the view model will notify our view model. The above does not do anything to the view model variable. Do I need to subscribe to the publisher and use sink to accomplish this?
Let's say you store a flag in UserDefaults to know whether the user has completed onboarding:
extension UserDefaults {
#objc dynamic public var completedOnboarding: Bool {
bool(forKey: "completedOnboarding")
}
}
You have a ViewModel which tells your View whether to show onboarding or not and has a method to mark onboarding as completed:
class ViewModel: ObservableObject {
#Published private(set) var showOnboarding: Bool = true
private let userDefaults: UserDefaults
public init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
self.showOnboarding = !userDefaults.completedOnboarding
userDefaults
.publisher(for: \.completedOnboarding)
.map { !$0 }
.receive(on: RunLoop.main)
.assign(to: &$showOnboarding)
}
public func completedOnboarding() {
userDefaults.set(true, forKey: "completedOnboarding")
}
}
To test this class you have a XCTestCase:
class MyTestCase: XCTestCase {
private var userDefaults: UserDefaults!
private var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
try super.setUpWithError()
userDefaults = try XCTUnwrap(UserDefaults(suiteName: #file))
userDefaults.removePersistentDomain(forName: #file)
}
// ...
}
Some of the test cases are synchronous for example you can easily test that showOnboarding depends on UserDefaults completedOnboarding property:
func test_whenCompletedOnboardingFalse_thenShowOnboardingTrue() {
userDefaults.set(false, forKey: "completedOnboarding")
let subject = ViewModel(userDefaults: userDefaults)
XCTAssert(subject.showOnboarding)
}
func test_whenCompletedOnboardingTrue_thenShowOnboardingFalse() {
userDefaults.set(true, forKey: "completedOnboarding")
let subject = ViewModel(userDefaults: userDefaults)
XCTAssertFalse(subject.showOnboarding)
}
Some test are asynchronous, which means you need to use XCTExpectations to wait for the #Published value to change:
func test_whenCompleteOnboardingCalled_thenShowOnboardingFalse() {
let subject = ViewModel(userDefaults: userDefaults)
// first define the expectation that showOnboarding will change to false (1)
let showOnboardingFalse = expectation(
description: "when completedOnboarding called then show onboarding is false")
// subscribe to showOnboarding publisher to know when the value changes (2)
subject
.$showOnboarding
.filter { !$0 }
.sink { _ in
// when false received fulfill the expectation (5)
showOnboardingFalse.fulfill()
}
.store(in: &cancellables)
// trigger the function that changes the value (3)
subject.completedOnboarding()
// tell the tests to wait for your expectation (4)
waitForExpectations(timeout: 0.1)
}

SwiftUI #Published Property Being Updated In DetailView

I am trying to understand if I am following the theory of SwiftUI #Published and #ObservedObject.
Theory
I have a Model that is receiving updates from a server. The model publishes any changes to the data in the model.
My main view observes this list from the model and creates a List view with cell views that pushes to a detail view. The cell views are published.
The detail view observes changes to the cell view.
What I Think Should Happen
When the model updates this would update the list view, which is does.
When the model updates the detail view would update if it was loaded. It does not.
Why doesn't the detail view update when the model updates if there is an #Published and #ObservedObject chain?
ObservableObjects don't nest. You have choices to trigger objectWillChange manually. That's actually a great thing because you can use an EnvironmentObject factory to wire up your app without exposing anything to views and not force everything to update all at once.
If you know it has changed from a callback you can fire it yourself objectWillChange.send().
You can also subscribe to a Publisher (e.g., another ObservableObjects ObjectWillChangePublisher, or some networking pipeline) and trigger the recipient ObservableObject's publisher on value receipt.
Here is a redux-style code example that goes hog wild and ties into every update.
import Foundation
import Combine
open class Republisher: ObservableObject {
public func republish() {
objectWillChange.send()
}
public init () {}
}
class VM: ObservableObject {
private var republishers = Set<AnyCancellable>()
internal var root: RootStore
init(_ root: RootStore, _ repubs: Republisher...) {
self.root = root
root.objectWillChange
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] _ in
guard let self = self else { return }
self.objectWillChange.send()
})
.store(in: &republishers)
repubs.forEach { repubs in
repubs.objectWillChange
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] _ in
guard let self = self else { return }
self.objectWillChange.send()
})
.store(in: &republishers)
}
}
deinit { republishers = [] }
}
import Foundation
import Combine
public final class RootStore: Republisher {
private var middlewareCancellables: Set<AnyCancellable> = []
public init(state: RootState,
reducer: #escaping Reducer<RootState, RootAction>,
middleware: Middlewares = []) {
self.state = state
self.reducer = reducer
self.middleware = RootStore.mandatoryWares(and: middleware)
}
public private(set) var state: RootState {
didSet { republish() }
}
...
}

Subscribe SwiftUI View to every update of a combine publisher

Here's a simplified example of an approach I want to take, but I can't get the simple example to work.
I have a Combine publisher who's subject is a view model State:
struct State {
let a: Bool
let b: Bool
let transition: Transition?
}
The State includes a transition property. This describes the Transition that the State made in order to become the current state.
enum Transition {
case onAChange, onBChange
}
I want to use transition property to drive animations in a View subscribed to the publisher so that different transitions animate in specific ways.
View code
Here's the code for the view. You can see how it tries to use the transition to choose an animation to update with.
struct TestView: View {
let model: TestViewModel
#State private var state: TestViewModel.State
private var cancel: AnyCancellable?
init(model: TestViewModel) {
self.model = model
self._state = State(initialValue: model.state.value)
self.cancel = model.state.sink(receiveValue: updateState(state:))
}
var body: some View {
VStack(spacing: 20) {
Text("AAAAAAA").scaleEffect(state.a ? 2 : 1)
Text("BBBBBBB").scaleEffect(state.b ? 2 : 1)
}
.onTapGesture {
model.invert()
}
}
private func updateState(state: TestViewModel.State) {
withAnimation(animation(for: state.transition)) {
self.state = state
}
}
private func animation(for transition: TestViewModel.Transition?) -> Animation? {
guard let transition = transition else { return nil }
switch transition {
case .onAChange: return .easeInOut(duration: 1)
case .onBChange: return .easeInOut(duration: 2)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView(model: TestViewModel())
}
}
Model code
final class TestViewModel: ObservableObject {
var state = CurrentValueSubject<State, Never>(State(a: false, b: false, transition: nil))
struct State {
let a: Bool
let b: Bool
let transition: Transition?
}
enum Transition {
case onAChange, onBChange
}
func invert() {
let oldState = state.value
setState(newState: .init(a: !oldState.a, b: oldState.b, transition: .onAChange))
setState(newState: .init(a: !oldState.a, b: !oldState.b, transition: .onBChange))
}
private func setState(newState: State) {
state.value = newState
}
}
You can see in the model code that when invert() is called, two state changes occur. The model first toggles a using the .onAChange transition, and then toggles b using the .onBChange transition.
What should happen
What should happen when this is run is that each time the view is clicked, the text "AAAAAAA" and "BBBBBBB" should toggle size. However, the "AAAAAAA" text should change quickly (1 second) and the "BBBBBBB" text should change slowly (2 seconds).
What actually happens
However, when I run this and click on the view, the view doesn't update at all.
I can see from the debugger that onTapGesture { … } is called and invert() is being called on the model. Also updateState(state:) is also being called. However, TestView is not changing on screen, and body is not invoked again.
Other things I tried
Using a callback
Instead of using a publisher to send the event to the view, I've tried a callback function in the model set to the view's updateState(state:) function. I assigned to this in the init of the view with model.handleUpdate = self.update(state:). Again, this did not work. The function invert() and update(state:) were called, as expected, but the view didn't actually change.
Using #ObservedObject
I change the model to be ObservableObject with its state being #Published. I set up the view to have an #ObservedOject for the model. With this, the view does update, but it updates both pieces of text using the same animation, which I don't want. It seems that the two state updates are squashed and it only sees the last one, and uses the transition from that.
Something that did work – sort of
Finally, I tried to directly copy the model's invert() function in to the view's onTapGesture handler, so that the view updates its own state directly. This did work! Which is something, but I don't want to put all by model update logic in my view.
Question
How can I have a SwiftUI view subscribe to all states that a model sends through its publisher so that it can use a transition property in the state to control the animation used for that state change?
The way you subscribe a view to the publisher is by using .onRecieve(_:perform:), so instead of saving a cancellable inside init, do this:
var body: some View {
VStack(spacing: 20) {
Text("AAAAAAA").scaleEffect(state.a ? 2 : 1)
Text("BBBBBBB").scaleEffect(state.b ? 2 : 1)
}
.onTapGesture {
model.invert()
}
.onReceive(model.state, perform: updateState(state:)) // <- here
}

How to prod a SwiftUI view to update when a model class sub-property changes?

I've created a trivial project to try to understand this better. Code below.
I have a source of data (DataSource) which contains a #Published array of MyObject items. MyObject contains a single string. Pushing a button on the UI causes one of the MyObject instances to update immediately, plus sets off a timer to update a second one a few seconds later.
If MyObject is a struct, everything works as I imagine it should. But if MyObject is a class, then the refresh doesn't fire.
My expectation is that changing a struct's value causes an altered instance to be placed in the array, setting off the chain of updates. However, if MyObject is a class then changing the string within a reference type leaves the same instance in the array. Array doesn't realise there has been a change so doesn't mention this to my DataSource. No UI update happens.
So the question is – what needs to be done to cause the UI to update when the MyObject class's property changes? I've attempted to make MyObject an ObservableObject and throw in some didchange.send() instructions but all without success (I believe these are redundant now in any case).
Could anyone tell me if this is possible, and how the code below should be altered to enable this? And if anyone is tempted to ask why I don't just use a struct, the reason is because in my actual project I have already tried doing this. However I am using collections of data types which modify themselves in closures (parallel processing of each item in the collection) and other hoops to jump through. I tried re-writing them as structs but ran in to so many challenges.
import Foundation
import SwiftUI
struct ContentView: View
{
#ObservedObject var source = DataSource()
var body: some View
{
VStack
{
ForEach(0..<5)
{i in
HelloView(displayedString: self.source.results[i].label)
}
Button(action: {self.source.change()})
{
Text("Change me")
}
}
}
}
struct HelloView: View
{
var displayedString: String
var body: some View
{
Text("\(displayedString)")
}
}
class MyObject // Works if declared as a Struct
{
init(label: String)
{
self.label = label
}
var label: String
}
class DataSource: ObservableObject
{
#Published var results = [MyObject](repeating: MyObject(label: "test"), count: 5)
func change()
{
print("I've changed")
results[3].label = "sooner"
_ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: {_ in self.results[1].label = "Or later"})
}
}
struct ContentView_Previews: PreviewProvider
{
static var previews: some View
{
ContentView()
}
}
When MyObject is a class type the results contains references, so when you change property of any instance inside results the reference of that instance is not changed, so results is not changed, so nothing published and UI is not updated.
In such case the solution is to force publish explicitly when you perform any change of internal model
class DataSource: ObservableObject
{
#Published var results = [MyObject](repeating: MyObject(label: "test"), count: 5)
func change()
{
print("I've changed")
results[3].label = "sooner"
self.objectWillChange.send() // << here !!
_ = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) {[weak self] _ in
self?.results[1].label = "Or later"
self?.objectWillChange.send() // << here !!
}
}
}

In SwiftUI, how to react to changes on "#Published vars" *outside* of a "View"

Suppose I have the following ObservableObject, which generates a random String every second:
import SwiftUI
class SomeObservable: ObservableObject {
#Published var information: String = ""
init() {
Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(updateInformation),
userInfo: nil,
repeats: true
).fire()
}
#objc func updateInformation() {
information = String("RANDOM_INFO".shuffled().prefix(5))
}
}
And a View, which observes that:
struct SomeView: View {
#ObservedObject var observable: SomeObservable
var body: some View {
Text(observable.information)
}
}
The above will work as expected.
The View redraws itself when the ObservableObject changes:
Now for the question:
How could I do the same (say calling a function) in a "pure" struct that also observes the same ObservableObject? By "pure" I mean something that does not conform to View:
struct SomeStruct {
#ObservedObject var observable: SomeObservable
// How to call this function when "observable" changes?
func doSomethingWhenObservableChanges() {
print("Triggered!")
}
}
(It could also be a class, as long as it's able to react to the changes on the observable.)
It seems to be conceptually very easy, but I'm clearly missing something.
(Note: I'm using Xcode 11, beta 6.)
Update (for future readers) (paste in a Playground)
Here is a possible solution, based on the awesome answer provided by #Fabian:
import SwiftUI
import Combine
import PlaygroundSupport
class SomeObservable: ObservableObject {
#Published var information: String = "" // Will be automagically consumed by `Views`.
let updatePublisher = PassthroughSubject<Void, Never>() // Can be consumed by other classes / objects.
// Added here only to test the whole thing.
var someObserverClass: SomeObserverClass?
init() {
// Randomly change the information each second.
Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(updateInformation),
userInfo: nil,
repeats: true
).fire() }
#objc func updateInformation() {
// For testing purposes only.
if someObserverClass == nil { someObserverClass = SomeObserverClass(observable: self) }
// `Views` will detect this right away.
information = String("RANDOM_INFO".shuffled().prefix(5))
// "Manually" sending updates, so other classes / objects can be notified.
updatePublisher.send()
}
}
class SomeObserverClass {
#ObservedObject var observable: SomeObservable
// More on AnyCancellable on: apple-reference-documentation://hs-NDfw7su
var cancellable: AnyCancellable?
init(observable: SomeObservable) {
self.observable = observable
// `sink`: Attaches a subscriber with closure-based behavior.
cancellable = observable.updatePublisher
.print() // Prints all publishing events.
.sink(receiveValue: { [weak self] _ in
guard let self = self else { return }
self.doSomethingWhenObservableChanges()
})
}
func doSomethingWhenObservableChanges() {
print(observable.information)
}
}
let observable = SomeObservable()
struct SomeObserverView: View {
#ObservedObject var observable: SomeObservable
var body: some View {
Text(observable.information)
}
}
PlaygroundPage.current.setLiveView(SomeObserverView(observable: observable))
Result
(Note: it's necessary to run the app in order to check the console output.)
The old way was to use callbacks which you registered. The newer method is to use the Combine framework to create publishers for which you can registers further processing, or in this case a sink which gets called every time the source publisher sends a message. The publisher here sends nothing and so is of type <Void, Never>.
Timer publisher
To get a publisher from a timer can be done directly through Combine or creating a generic publisher through PassthroughSubject<Void, Never>(), registering for messages and sending them in the timer-callback via publisher.send(). The example has both variants.
ObjectWillChange Publisher
Every ObservableObject does have an .objectWillChange publisher for which you can register a sink the same as you do for Timer publishers. It should get called every time you call it or every time a #Published variable changes. Note however, that is being called before, and not after the change. (DispatchQueue.main.async{} inside the sink to react after the change is complete).
Registering
Every sink call creates an AnyCancellable which has to be stored, usually in the object with the same lifetime the sink should have. Once the cancellable is deconstructed (or .cancel() on it is called) the sink does not get called again.
import SwiftUI
import Combine
struct ReceiveOutsideView: View {
#if swift(>=5.3)
#StateObject var observable: SomeObservable = SomeObservable()
#else
#ObservedObject var observable: SomeObservable = SomeObservable()
#endif
var body: some View {
Text(observable.information)
.onReceive(observable.publisher) {
print("Updated from Timer.publish")
}
.onReceive(observable.updatePublisher) {
print("Updated from updateInformation()")
}
}
}
class SomeObservable: ObservableObject {
#Published var information: String = ""
var publisher: AnyPublisher<Void, Never>! = nil
init() {
publisher = Timer.publish(every: 1.0, on: RunLoop.main, in: .common).autoconnect().map{_ in
print("Updating information")
//self.information = String("RANDOM_INFO".shuffled().prefix(5))
}.eraseToAnyPublisher()
Timer.scheduledTimer(
timeInterval: 1.0,
target: self,
selector: #selector(updateInformation),
userInfo: nil,
repeats: true
).fire()
}
let updatePublisher = PassthroughSubject<Void, Never>()
#objc func updateInformation() {
information = String("RANDOM_INFO".shuffled().prefix(5))
updatePublisher.send()
}
}
class SomeClass {
#ObservedObject var observable: SomeObservable
var cancellable: AnyCancellable?
init(observable: SomeObservable) {
self.observable = observable
cancellable = observable.publisher.sink{ [weak self] in
guard let self = self else {
return
}
self.doSomethingWhenObservableChanges() // Must be a class to access self here.
}
}
// How to call this function when "observable" changes?
func doSomethingWhenObservableChanges() {
print("Triggered!")
}
}
Note here that if no sink or receiver at the end of the pipeline is registered, the value will be lost. For example creating PassthroughSubject<T, Never>, immediately sending a value and aftererwards returning the publisher makes the messages sent get lost, despite you registering a sink on that subject afterwards. The usual workaround is to wrap the subject creation and message sending inside a Deferred {} block, which only creates everything within, once a sink got registered.
A commenter notes that ReceiveOutsideView.observable is owned by ReceiveOutsideView, because observable is created inside and directly assigned. On reinitialization a new instance of observable will be created. This can be prevented by use of #StateObject instead of #ObservableObject in this instance.