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

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()
}
}

Related

#EnvironmentObject bug in SwiftUI

When I press the Move button in the contextMenu, I change the isCopied and setOriginPath variables in the EnvironmentObject. When this change is made, the List view is cleared and I can't see anything on the screen. I don't have any problems when I don't use EnvironmentObject.
ContextMenu:
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
View:
struct DetailObjectView: View {
#ObservedObject var safeFileVM: SafeFileViewModel = SafeFileViewModel()
#EnvironmentObject var safeFileClipboard: SafeFileClipBoard
var currentFile: MyFile
var currentLocation = ""
var body: some View {
VStack {
.....
}
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
}
}
In the mini project below, when the EnvironmentObject value changes, navigation goes to the beginning. Why ? How can I fix this ?
Example Project:
Main:
#main
struct EnvironmentTestApp: App {
#StateObject var fooConfig: FooConfig = FooConfig()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.environmentObject(fooConfig)
}
}
}
}
ContentView:
struct ContentView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
struct Number: Identifiable {
var id = UUID()
var item: String
}
DetailView:
struct DetailView: View {
#EnvironmentObject var fooConfig: FooConfig
var itemNumber: String = ""
var body: some View {
NavigationLink(destination: ContentView().environmentObject(fooConfig)) {
Text("\(itemNumber) - \(fooConfig.fooBool == true ? "On" : "Off")")
.environmentObject(fooConfig)
.contextMenu {
Button {
fooConfig.fooBool.toggle()
} label: {
HStack {
Text(fooConfig.fooBool != true ? "On" : "Off")
}
}
}
}
}
}
ObservableObject:
class FooConfig: ObservableObject {
#Published var fooBool: Bool = false
}
Move that from scene into ContentView, because scene is a bad place to update view hierarchy, it is better to do inside view hierarchy, so here
struct EnvironmentTestApp: App {
var body: some Scene {
WindowGroup {
ContentView() // only root view here !!
}
}
}
everything else is inside views, like
struct ContentView: View {
#StateObject private var foo = FooConfig()
var body: some View {
NavigationView {
MainView()
.environmentObject(foo) // << here !!
}
}
}
struct MainView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
and so on...
Tested with Xcode 14 / iOS 16

Problems with timer and views in 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
}
}
}
}

Passing steper value to another View - SwiftUI

I have very simple "app" in SwiftUI
How i can passing stepper value from list to struct SumOfValue or to ContentView ? But i want passing sum of stepper value in case from image will be 8.
struct ContentView: View {
var body: some View {
VStack{
List{
ProductList()
ProductList()
}
Spacer()
Text("Sum of stepper value: ?????")
.padding(.bottom, 50
)
SumOfValue()
}
}
}
struct ProductList:View {
#State var stepperValueTest: Int = 0
var body: some View {
HStack {
Stepper("Value: \(stepperValueTest)", value: $stepperValueTest)
}
}
}
struct SumOfValue: View {
var body: some View {
Text("or here sum of value: ????? ")
.foregroundColor(Color.red)
}
}
I try use #Binding but didn`t work.
There are multiple approaches here, and it's ultimately a question of data organization.
One way to think about is that there is an array of values that a parent - ContentView in your case - "owns", and each child updates their allotted value in that array using a binding. This way, the parent can easily calculate the sum of these values since it has the entire array.
struct ContentView: View {
#State var values = [0,0,0]
var body: some View {
VStack {
List {
ProductList(stepperValueTest: $values[0])
ProductList(stepperValueTest: $values[1])
ProductList(stepperValueTest: $values[2])
}
Text("Sum: \(sum)")
}
}
var sum: Int { values.reduce(0, +) }
}
struct ProductList:View {
#Binding var stepperValueTest: Int // change to Binding
var body: some View {
HStack {
Stepper("Value: \(stepperValueTest)", value: $stepperValueTest)
}
}
}
The #State goes in the parent (ContentView), and the #Binding goes in the child (ProductList and SumOfValue).
Try this:
struct ContentView: View {
/// States here!
#State var firstStepperValue: Int = 0
#State var secondStepperValue: Int = 0
var body: some View {
VStack{
List{
/// pass it in here!
ProductList(stepperValueTest: $firstStepperValue)
ProductList(stepperValueTest: $secondStepperValue)
}
Spacer()
/// add the values here
Text("Sum of stepper value: \(firstStepperValue + secondStepperValue)")
.padding(.bottom, 50
)
/// you can also pass it in here
SumOfValue(first: $firstStepperValue, second: $secondStepperValue)
.padding(.bottom, 100)
}
}
}
struct ProductList:View {
/// Binding here!
#Binding var stepperValueTest: Int
var body: some View {
HStack {
Stepper("Value: \(stepperValueTest)", value: $stepperValueTest)
}
}
}
struct SumOfValue: View {
#Binding var first: Int
#Binding var second: Int
var body: some View {
Text("or here sum of value: \(first + second) ")
.foregroundColor(Color.red)
}
}
Result:

How to display another view from an existing view in SwiftUI

I have an existing view displayed. After displaying that view for 2 seconds, I want to navigate or display another view. The following code does not work.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
UIGraphicsBeginImageContext(self.view.frame.size)
UIImage(named: "ProfileSplashScreen")?.draw(in: self.view.bounds)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
self.view.backgroundColor = UIColor(patternImage: image)
DispatchQueue.main.asyncAfter(deadline: .now()+2.0) {
GameSelectorController()
}
//=====================================================
import SwiftUI
struct GameSelectorController: UIViewController {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
}
}
struct GameSelectorController_Previews: PreviewProvider {
static var previews: some View {
GameSelectorController()
}
}
In pure SwiftUI you would do something like this:
struct ContentView: View {
#State private var showFirst = true
var body: some View {
Group {
if showFirst {
StartView()
} else {
SecondView()
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.showFirst = false
}
}
}
}
struct StartView: View {
var body: some View {
Text("First")
}
}
struct SecondView: View {
var body: some View {
Text("Second")
}
}

How to update a view depending on button click from other view in SwiftUI

I often face this situation but so far I could not find a good solution. Thing is when my SwiftUI View gets bigger I refactor the code by making another struct and call the struct in the respective view. Say I got a struct A and I refactor some code in struct B, but how can I update the view or call a function in struct A depending on button click on struct B ? The below code might help to understand the situation:
import SwiftUI
struct ContentView: View {
#State var myText: String = "Hello World"
#State var isActive: Bool = false
var body: some View {
VStack {
Text(self.myText)
AnotherStruct(isActive: $isActive)
}
.onAppear {
if self.isActive == true {
self.getApi()
}
}
}
func getApi() {
print("getApi called")
self.myText = "Hello Universe"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AnotherStruct: View {
#Binding var isActive: Bool
var body: some View {
VStack {
Button( action: {
self.isActive.toggle()
}) {
Text("Button Tapped")
}
}
}
}
Here is a demo of possible approach to solve such cases - with separated responsibility and delegated activity.
struct ContentView: View {
#State var myText: String = "Hello World"
var body: some View {
VStack {
Text(self.myText)
AnotherStruct(onActivate: getApi)
}
}
func getApi() {
print("getApi called")
self.myText = "Hello Universe"
}
}
struct AnotherStruct: View {
let onActivate: () -> ()
// #AppStorage("isActive") var isActive // << possible store in defaults
var body: some View {
VStack {
Button( action: {
// self.isActive = true
self.onActivate()
}) {
Text("Button Tapped")
}
}
// .onAppear {
// if isActive {
// self.onActivate()
// }
// }
}
}