Problems with timer and views in SwiftUI - swiftui

I try to develop an WatchOS app, where the views are feeded by a timer:
import SwiftUI
import Combine
class AcidityTimer: ObservableObject {
#Published var num: Int = 0
private var subscription: AnyCancellable?
init() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.num += 1
if self.num == 101 {
self.num = 0
}
}
}
}
struct AcidityTextView: View {
#EnvironmentObject var acTimer: AcidityTimer
#State var acidity: Double = 0.0
var body: some View {
Text(String(format: "Acidity: %.1f", acidity))
.onReceive(acTimer.$num) { result in
acidity = Double(result) / 10.0
}
}
}
struct AcidityGaugeView: View {
#State var acidity = 0.0
#EnvironmentObject var acTimer: AcidityTimer
var body: some View {
Gauge(value: acidity, in: 0...10) {
Image(systemName: "drop.fill")
.foregroundColor(.green)
} currentValueLabel: {
Text("\(acidity, specifier: "%.1f")")
} minimumValueLabel: {
Text("0")
} maximumValueLabel: {
Text("10")
}
.gaugeStyle(CircularGaugeStyle())
.onReceive(acTimer.$num) { result in
acidity = Double(result) / 10.0
}
}
}
struct ContentView: View {
#ObservedObject var acTimer = AcidityTimer()
#State var counter: Int = 0
var body: some View {
TabView {
AcidityTextView()
.environmentObject(acTimer)
AcidityGaugeView()
.environmentObject(acTimer)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The app itself runs fine on the Apple Watch simulator, but I get the error message:
[SwiftUI] Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
Any idea what's wrong?

You use timer on initial and nowhere didn't stopped or update
You can change implementation to
struct ContentView: View {
#State var timeRemaining = 10
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
}
}
}
}

Related

#Published value don't pass through views

I'm beggining with SwiftUI and I wanted to develop a small simple app to practice. I have a problem with #Published property that don't pass through views and so don't update the view.
I explain : In the first view I calculate the vMoyenne property and update it. I wanted to show this value in the next view ("Passage") to be able to use it for some other calculation but I tried many thing and the value in the "Passage" View doesn't update...
Here is the code :
ContentView.swift :
struct ContentView: View {
var body: some View {
TabView {
SpeedView().tabItem {
Label("Vitesse", systemImage: "figure.run.circle.fill")
}
PassageView(parameters: Parameters()).tabItem {
Label("Passage", systemImage: "timer.circle.fill")
}
}
}
}
Parameters.swift
class Parameters: ObservableObject {
#Published var distance: Double?
static let units = ["m", "km"]
#Published var unit = 1
#Published var hour: Int = 0
#Published var minute: Int = 0
#Published var second: Int = 0
#Published var vMoyenne = 0.0
#Published var allure = 0.0
#Published var convertedDecimalToSeconds = 0
var time: Int?
...
func calcVMoy() -> Void{
var d = distance!
let t = Double(time!) / 3600
var unite: String {
return Parameters.units[unit]
}
var calc = 0.0
if unite == "km" {
calc = d / t
} else {
d = d / 1000
calc = d / t
}
vMoyenne = calc
}
...
init() {
}
}
**SpeedView.swift **
struct SpeedView: View {
#ObservedObject var parameters = Parameters()
...
...
Button {
showVMoy = true
disableChange = true
if parameters.distance == nil {
parameters.distance = 0
} else {
parameters.runCalc()
}
} label: {
Text("Calculer")
}
... *// Here I can show and see the calculated vMoyenne property without problem...*
...
}
And the PassageView.swift where I want to show the vMoyenne property...
struct PassageView: View {
#ObservedObject var parameters:Parameters
var body: some View {
Text("\(parameters.vMoyenne)") *//want to show the vMoyenne value that we calculate previously but it always show 0,000...*
}
}
Thanks a lot for your help !!
PS : I tried many things like using didSet but I don't understand what I did wrong...
I found some post on stackoverflow but when I tried it doesn't work...
If you update the ContentView to it should work. The problem was that the SpeedView and PassageView were not sharing the same parameters object
struct ContentView: View {
#StateObject var parameters: Parameters = .init()
var body: some View {
TabView {
SpeedView(parameters: parameters).tabItem {
Label("Vitesse", systemImage: "figure.run.circle.fill")
}
PassageView(parameters: parameters).tabItem {
Label("Passage", systemImage: "timer.circle.fill")
}
}
}
}

SwiftUI: how to observe a simple #State Int value?

I would like to observe the changes of a simple value:
#main
struct TestSwiftUIApp: App {
#State var test: Int = 0
var body: some Scene {
WindowGroup {
ContentView(value: $test)
}
}
init() {
fetchNewTest()
}
func fetchNewTest() {
test = test == 0 ? 1 : 0
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.fetchNewTest()
}
}
}
struct ContentView: View {
#Binding var value: Int
var body: some View {
Text("\(value)")
.padding()
}
}
However, nothing happens in fetchNewTest() so I don't really understand what happens with this variable. Could you please help me?
Thank you for your help
Correct your code to this down way and use onAppear, you cannot and you should not use init of views in SwiftUI.
#main
struct TestSwiftUIApp: App {
#State var test: Int = 0
var body: some Scene {
WindowGroup {
ContentView(value: $test)
.onAppear() { fetchNewTest() } // <<: Here!
}
}
func fetchNewTest() {
test = test == 0 ? 1 : 0
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.fetchNewTest()
}
}
}
struct ContentView: View {
#Binding var value: Int
var body: some View {
Text("\(value)")
.padding()
}
}

SwiftUI Stepper save value

How can I save the value of the stepper in the UserDefaults every time I press + or -?
I don't want a separate save button.
struct Settings: View {
#State private var zielwert = UserDefaults.standard.integer(forKey: "zielwert")
var body: some View {
VStack {
Stepper("Zielwert \(zielwert)", value: $zielwert, in: 90...160)
Button("Speichern") {
UserDefaults.standard.set(self.zielwert, forKey: "zielwert")
}
}
.padding(.horizontal, 23.0)
}
}
SwiftUI 2
Use AppStorage:
struct Settings: View {
#AppStorage("zielwert") private var zielwert = 90
var body: some View {
VStack {
Stepper("Zielwert \(zielwert)", value: $zielwert, in: 90...160)
}
.padding(.horizontal, 23.0)
}
}
SwiftUI 1
Use .onReceive:
import Combine
struct Settings: View {
#State private var zielwert = UserDefaults.standard.integer(forKey: "zielwert")
var body: some View {
VStack {
Stepper("Zielwert \(zielwert)", value: $zielwert, in: 90...160)
.onReceive(Just(zielwert)) { zielwert in
print(zielwert)
UserDefaults.standard.set(zielwert, forKey: "zielwert")
}
}
.padding(.horizontal, 23.0)
}
}

SwiftUI can not update class data updates

I came across a situation that you use class data as your data source, and display them in a swiftUI list view, when you update your data source, the swiftUI list view won't be updated, what can we do to make the class data updates interactive with swiftUI?
see code blow:
I define the environment object :
import Foundation
import Combine
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
data.objectWillChange.send()
}
self.objectWillChange.send()
}
}
to display each data in a Row View:
import SwiftUI
struct RowView: View {
#State var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
struct RowView_Previews: PreviewProvider {
static var previews: some View {
RowView(data: RowData(title: "text", count: 1))
}
}
class RowData: ObservableObject {
var title: String = ""
var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
in content view, display the data in a list view, I would like to refresh all the view updates when click update button. the button triggers the update methods to update the class data value from data source.
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var shouldUpdate:Bool = false
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.shouldUpdate.toggle()
self.localData.removeAll()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
}
}
Well... I don't see the reason to have localData, but, anyway, here is modified code that works.
Tested with Xcode 12 / iOS 14
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
}
self.objectWillChange.send()
}
}
struct RowView: View {
#ObservedObject var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
class RowData: ObservableObject {
#Published var title: String = ""
#Published var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
.onAppear {
self.data.fetch()
self.localData = self.data.datalist
}
}
}

Invalidate List SwiftUI

Workaround at bottom of Question
I thought SwiftUI was supposed to automatically update views when data they were dependent on changed. However that isn't happening in the code below:
First I make a simple BindableObject
import SwiftUI
import Combine
class Example: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var test = 1 {
didSet {
didChange.send(())
}
}
}
Then the root view of the app:
struct BindTest : View {
#Binding var test: Example
var body: some View {
PresentationButton(destination: BindChange(test: $test)) {
ForEach(0..<test.test) { index in
Text("Invalidate Me! \(index)")
}
}
}
}
And finally the view in which I change the value of the BindableObject:
struct BindChange : View {
#Binding var test: Example
#Environment(\.isPresented) var isPresented
var body: some View {
Button(action: act) {
Text("Return")
}
}
func act() {
test.test = 2
isPresented?.value = false
}
}
When the return button is tapped there should be 2 instances of the Text View - but there is only 1. What am I doing wrong?
Also worth noting: If I change the #Binding to #EnvironmentObject the program just crashes when you tap the button producing this error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Full code below:
import SwiftUI
import Combine
class Example: BindableObject {
var didChange = PassthroughSubject<Example, Never>()
var test = 1 {
didSet {
didChange.send(self)
}
}
static let `default` = {
return Example()
}()
}
//Root View
struct BindTest : View {
#EnvironmentObject var test: Example
var body: some View {
PresentationButton(destination: BindChange()) {
ForEach(0..<test.test) { t in
Text("Invalidate Me! \(t)")
}
}
}
}
//View that changes the value of #Binding / #EnvironmentObject
struct BindChange : View {
#EnvironmentObject var test: Example
#Environment(\.isPresented) var isPresented
var body: some View {
Button(action: act) {
Text("Return")
}
}
func act() {
test.test = 2
isPresented?.value = false
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
//ContentView().environmentObject(EntryStore())
BindTest().environmentObject(Example())
}
}
#endif
EDIT 2: Post's getting a little messy at this point but the crash with EnvironmentObject seems to be related to an issue with PresentationButton
By putting a NavigationButton inside a NavigationView the following code produces the correct result - invalidating the List when test.test changes:
//Root View
struct BindTest : View {
#EnvironmentObject var test: Example
var body: some View {
NavigationView {
NavigationButton(destination: BindChange()) {
ForEach(0..<test.test) { t in
Text("Functional Button #\(t)")
}
}
}
}
}