How to update a complicated model of a View from within its body - state

I am building a custom View struct with a large and complicated model that I want to update from within the var body : some View { ... } property (ex. tapping on a button representing a table column in the view should change the sort order of the rows in the table). I am not allowed to modify this model from within body because self is immutable however:
struct MyTable : View {
struct Configuration {
// a LOT of data here, ie header, rows, footer, style, etc
mutating func update() { ... } // this method must be called before view render
}
var configuration : Configuration
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // error, `self` is immutable
self.configuration.update() // error, `self` is immutable
}
}
I really don't want to create #State variables for all of the configuration data because 1) there's a lot of configuration data, 2) it would be difficult to model the model in such a way.
I tried instead making configuration a #State var, but I am unable to set the configuration object at init() time even though the code compiles and runs! (BTW, the configuration var now needs to be initialized before init, otherwise I get an error on the line self.configuration = c that self is used before all members are initialized -- this is likely a complication with using #State which is a property wrapper.)
struct MyTable : View {
struct Configuration {
...
}
#State var configuration : Configuration = Configuration() // must initialize the #State var
init(configuration c: Configuration) {
self.configuration = c // does not assign `c` to `configuration` !!!
self.$configuration.wrappedValue = c // this also does not assign !!!
self.configuration.update()
}
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // ok, no error now about mutating a #State var
self.configuration.update() // ok, no error
}
}

I came up with a work around where I don't need to call update() in MyTable.init() by creating a custom init() in Configuration that will call update(). This way the init() in MyTable is unnecessary and this approach resolves all previously encountered problems:
struct MyTable : View {
struct Configuration {
...
init(...) {
...
self.update()
}
mutating func update() { ... } // this method must be called before view render
}
#State var configuration : Configuration // note that this property can't be marked private because we want to inject `configuration` at init
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // ok, no error now about mutating a #State var
self.configuration.update() // ok, no error
...
}
}
Then in my calling code:
return MyTable.init(configuration: MyTable.Configuration.init(...))

Related

Creating a List of Toggle From a Set Using ForEach in swiftui

I' trying to create a set of Toggles, that need to be stored in one core data field of type "Transformable". I started with this example:
https://developer.apple.com/forums/thread/118595
in combination with other ideas from stack.
I'm trying to get this way:
Create a Set of structs like this
struct AllScopes: Identifiable, Hashable {
var id: UUID
var name: String
var notify: Bool
}
[...]
// all the stuff with View and body with
#State var scopes = Set<AllScopes>()
[...]
// and here I run through my FetchRequest to fill the Set
.onAppear {
for scope in allScopes {
scopes.insert(
AllScopes(
id: scope.id!,
name: scope.name!,
notify: false
)
)
}
}
In the end I've got a nice Set with all my scopes.
I call a new View with YearlyReportPage6(scopes: $scopes)
And now my problem - the next view:
struct YearlyReportPage6: View {
#Binding var scopes: Set<AllScopes>
init(scopes: Binding<Set<AllScopes>>) {
_scopes = scopes
}
var body: some View {
VStack {
ForEach(scopes.indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
}
}
}
But all in ForEach creates errors. Either Binding in isOn: is wrong, or ForEach can't work with the set, or the Text is not a String, or, or, or...
In the end there should be a list of Toggles (checkboxes) and the selection should be stored in database.
Changing the Set into a simple Array like #State var scopes = [AllScopes]() will work for the Toggles, but how can I store this into a Transformable?
ForEach(Array(scopes).indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
To summarize:
either how can I create the list of Toggles with the Set of AllScopes
or how can I store the Array / Dictionary into the Transformable field?
I hope, you can understand my clumsy English. :-)

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 !!
}
}
}

AVSpeechSynthesizerDelegate implementation in 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.

SwiftUI - How do I create Global #State Variables?

Is it possible to create a global #State variable in SwiftUI that can be accessed across multiple Swift UI files?
I've looked into #EnvironmentObject variables but can't seem to make them do what I want them to do.
As of Beta 3 you cannot create a top-level global #State variable. The compiler will segfault. You can place one in a struct and create an instance of the struct in order to build. However, if you actually instantiate that you'll get a runtime error like: Accessing State<Bool> outside View.body.
Probably what you're looking for is an easy way to create a binding to properties on a BindableObject. There's a good example of that in this gist.
It is possible to create a Binding to a global variable, but unfortunately this still won't do what you want. The value will update, but your views will not refresh (code example below).
Example of creating a Binding programmatically:
var globalBool: Bool = false {
didSet {
// This will get called
NSLog("Did Set" + globalBool.description)
}
}
struct GlobalUser : View {
#Binding var bool: Bool
var body: some View {
VStack {
Text("State: \(self.bool.description)") // This will never update
Button("Toggle") { self.bool.toggle() }
}
}
}
...
static var previews: some View {
GlobalUser(bool: Binding<Bool>(getValue: { globalBool }, setValue: { globalBool = $0 }))
}
...