I'm trying to re create an older version of my Onboarding setup with the new SwiftUI and when I try to share the state so the view changes, it simply doesn't know that something has changed, this is what I'm doing:
In the main .swift struct (not ContentView.swift) I defined the pages like this:
#main
struct AnotherAPP: App {
#ObservedObject var onBoardingUserDefaults = OnBoardingUserDefaults()
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
// Onboarding screen
if (onBoardingUserDefaults.isOnBoardingDone == false) {
OnboardingPageView()
} else {
UserLoginView()
}
}
}
}
So on the onBoarding page when I click the button to go to the login, it stores it, but it doesn't actually refreshes the view. There (in the OnboardingPageView.swift) I call the UserDefaults like this:
#ObservedObject private var onBoardingUserDefaults = OnBoardingUserDefaults()
and on the button I change it like this:
self.onBoardingUserDefaults.isOnBoardingDone = true
UserDefaults.standard.synchronize()
So what's going on?
I know for instance if I create a #State on the #main and I bind it to the OnboardingPageView it works, as soon as I hit that button it takes me there.
You can use AppStorage to manage UserDefaults variable in multiple views:
#main
struct TestApp: App {
#AppStorage("isOnBoardingDone") var isOnBoardingDone = false
var body: some Scene {
WindowGroup {
if !isOnBoardingDone {
OnboardingPageView(isOnBoardingDone: $isOnBoardingDone)
} else {
UserLoginView()
}
}
}
}
struct OnboardingPageView: View {
#Binding var isOnBoardingDone: Bool
var body: some View {
Button("Complete") {
isOnBoardingDone = true
}
}
}
If I understood correctly, you are trying to pass the value of a state variable in the Content View to another view in the same app. For simplicity, Let's say your variable is initialised as follows in ContentView:
#State private var countryIndex = 0 //Assuming the name of the variable is countryIndex
Now, to transfer the value write the following in the Content View (or wherever the variable is initially):
//Other code
NavigationLink(destination: NextPage(valueFromContentView: $countryIndex)) {
Text("Moving On")
}//In this case, the variable that will store the value of countryIndex in the other view is called valueFromContentView
//Close your VStacks and your body and content view with a '}'
In your second view or the other view, initialise a Binding variable called valueFromContentView using:
#Binding var valueFromContentView: Int
Then, scroll down to the code that creates your previews. FYI, It is another struct called ViewName_Previews: PreviewProvider { ... }
IF you haven't changed anything, it will be:
struct NextPage_Previews: PreviewProvider {
static var previews: some View {
}
}
Remember, my second view is called NextPage.
Inside the previews braces, enter the code:
NextPage(valueFromContentView: .constant(0))
So, the code that creates the preview for your application now looks like:
struct NextPage_Previews: PreviewProvider {
static var previews: some View {
NextPage(valueFromContentView: .constant(0)) //This is what you add
}
}
Remember, NextPage is the name of my view and valueFromContentView is teh binding variable that I initialised above
Like this, you now can transfer the value of a variable in one view to another view.
Related
I'm trying to get into swift/swiftui but I'm really struggling with this one:
I have a MainView containing a ChildView. The ChildView has a function update to fetch the data to display from an external source and assign it to a #State data variable.
I'd like to be able to trigger update from MainView in order to update data.
I've experienced that update is in fact called, however, data is reset to the initial value upon this call.
The summary of what I have:
struct ChildView: View {
#State var data: Int = 0
var body: some View {
Text("\(data)")
Button(action: update) {
Text("update") // works as expected
}
}
func update() {
// fetch data from external source
data = 42
}
}
struct MainView: View {
var child = ChildView()
var body: some View {
VStack {
child
Button(action: {
child.update()
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
When googling for a solution, I only came across people suggesting to move update and data to MainView and then pass a binding of data to ChildView.
However, following this logic I'd have to blow up MainView by adding all the data access logic in there. My point of having ChildView at all is to break up code into smaller chunks and to reuse ChildView including the data access methods in other parent views, too.
I just cannot believe there's no way of doing this in SwiftUI.
Is completely understandable to be confused at first with how to deal with state on SwiftUI, but hang on there, you will find your way soon enough.
What you want to do can be achieved in many different ways, depending on the requirements and limitations of your project.
I will mention a few options, but I'm sure there are more, and all of them have pros and cons, but hopefully one can suit your needs.
Binding
Probably the easiest would be to use a #Binding, here a good tutorial/explanation of it.
An example would be to have data declared on your MainView and pass it as a #Binding to your ChildView. When you need to change the data, you change it directly on the MainView and will be reflected on both.
This solutions leads to having the logic on both parts, probably not ideal, but is up to what you need.
Also notice how the initialiser for ChildView is directly on the body of MainView now.
Example
struct ChildView: View {
#Binding var data: Int
var body: some View {
Text("\(data)")
Button(action: update) {
Text("update") // works as expected
}
}
func update() {
// fetch data from external source
data = 42
}
}
struct MainView: View {
#State var data: Int = 0
var body: some View {
VStack {
ChildView(data: $data)
Button(action: {
data = 42
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
ObservableObject
Another alternative would be to remove state and logic from your views, using an ObservableObject, here an explanation of it.
Example
class ViewModel: ObservableObject {
#Published var data: Int = 0
func update() {
// fetch data from external source
data = 42
}
}
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Text("\(viewModel.data)")
Button(action: viewModel.update) {
Text("update") // works as expected
}
}
}
struct MainView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
ChildView(viewModel: viewModel)
Button(action: {
viewModel.update()
}) {
Text("update") // In fact calls the function, but doesn't set the data variable to the new value
}
}
}
}
I am relatively new to SwiftUI and I'm trying to work on my first app. I am trying to use a segmented picker to give the user an option of changing between a DayView and a Week View. In each on of those Views, there would be specific user data that whould be shown as a graph. The issue I am having is loading the data. I posted the code below, but from what I can see, the issue comes down to the following:
When the view loads in, it starts with loading the dayView, since the selectedTimeInterval = 0. Which is fine, but then when the users presses on the "Week" in the segmented Picker, the data does not display. This due to the rest of the View loading prior to the .onChange() function from the segmented picker running. Since the .onChange is what puts the call into the viewModel to load the new data, there is no data. You can see this in the print statements if you run the code below.
I would have thought that the view load order would have been
load segmented picker
run the .onChange if the value changed
load the rest of the view
but the order actual is
load segmented picker,
load the rest of the view (graph loads with no data here!!!!!)
run the .onChange if the value has changed.
I am pretty lost so any help would be great! Thank you so much!
import SwiftUI
import OrderedCollections
class ViewModel: ObservableObject{
#Published var testDictionary: OrderedDictionary<String, Int> = ["":0]
public func daySelected() {
testDictionary = ["Day View Data": 100]
}
public func weekSelected() {
testDictionary = ["Week View Data": 200]
}
}
struct ContentView: View {
#State private var selectedTimeInterval = 0
#StateObject private var vm = ViewModel()
var body: some View {
VStack {
Picker("Selected Date", selection: $selectedTimeInterval) {
Text("Day").tag(0)
Text("Week").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: selectedTimeInterval) { _ in
let _ = print("In on change")
//Logic to handle different presses of the selector
switch selectedTimeInterval {
case 0:
vm.daySelected()
case 1:
vm.weekSelected()
default:
print("Unknown Selected Case")
}
}
switch selectedTimeInterval {
case 0:
let _ = print("In view change")
Day_View()
case 1:
let _ = print("In view change")
Week_View(inputDictionary: vm.testDictionary)
default:
Text("Whoops")
}
}
}
}
struct Day_View: View {
var body: some View {
Text("Day View!")
}
}
struct Week_View: View {
#State private var inputDictionary: OrderedDictionary<String,Int>
init(inputDictionary: OrderedDictionary<String,Int>) {
self.inputDictionary = inputDictionary
}
var body: some View {
let keys = Array(inputDictionary.keys)
let values = Array(inputDictionary.values)
VStack {
Text(keys[0])
Text(String(values[0]))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In your WeekView, change
#State private var inputDictionary: OrderedDictionary<String,Int>
to
private let inputDictionary: OrderedDictionary<String,Int>
#State is for the local state of the view. The idea is that you are initing it with initial state and from then on the view itself will change it and cause re-renders. When the WeekView is re-rendered, SwiftUI is ignoring the parameter you pass into the init and copying it from the previous WeekView to maintain state.
But, you want to keep passing in the dictionary from ContentView and cause re-renders from the parent view.
The main issue is that the initialization of the #State property wrapper is wrong.
You must use this syntax
#State private var inputDictionary: OrderedDictionary<String,Int>
init(inputDictionary: OrderedDictionary<String,Int>) {
_inputDictionary = State(wrappedValue: inputDictionary)
}
Or – if inputDictionary is not going to be modified – just declare it as (non-private) constant and remove the init method
let inputDictionary: OrderedDictionary<String,Int>
I want to have my content view display data that is global to the app and manipulated outside of the content view itself.
Does swift have a binding to allow outside variables?
I have created what I think is the most basic of applications:
//
// myTestxApp.swift
// myTestx
import SwiftUI
var myStng = "Hello\n"
var myArray = ["Hello\n"]
func myTest(){
myStng.append("Hello\n")
myArray.append("Hello\n")
print(myStng,myArray)
}
#main
struct myTestxApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
//
// ContentView.swift
// myTestx
import SwiftUI
struct ContentView: View {
#State var i = myStng
#State var j = myArray
var body: some View {
VStack{
Button( action: myTest ){ Text("Update") }
List{ Text(i).padding() }
List{ ForEach(myArray, id: \.self)
{ i in Text(i).padding()} }
} //end VStack
} //end View
} //end ContentView
I declare two app global variables, have an external function where they are updated, and for this example, a view with a calling button to the function and List areas for the updated results tied via #State variables. In my planned app, the update functions would be part of the data processing activity. I want to be able to edit data and have the content view(s) update displayed data when that data item is updated. In this example:
Code compiles and runs, with the console showing two variables being updated, but the view controller is not responding to the state change? Is #State the appropriate binding to use or should I use some other method to cause the content view items to recognize content change?
Any pointers would be greatly appreciated.
As other fellows suggested, you have to be very careful about using global variables, you should expose them only to the scope needed.
The problem is that you are treating #State var i = myStng thinking this would create a reactive connection between i and myStng, but that is not true. This line is creating a reactive connection between i and a memory address that SwiftUI manages for you, with its first value being what myStng is at that exact moment.
Anyway, I am posting an example of how can you achieve your goal using Environment Object with your provided code.
import SwiftUI
class GlobalVariables: ObservableObject{
#Published var myStng = "Hello\n"
#Published var myArray = ["Hello\n"]
}
func myTest(variables: GlobalVariables){
variables.myStng.append("Hello\n")
variables.myArray.append("Hello\n")
}
#main
struct myTestxApp: App {
#StateObject var globalEnvironment = GlobalVariables()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(globalEnvironment)
}
}
}
//
// ContentView.swift
// myTestx
//import SwiftUI
struct ContentView: View {
#EnvironmentObject var global: GlobalVariables
var body: some View {
VStack{
Button {
myTest(variables: global)
} label: {
Text("Update")
}
List{ Text(global.myStng).padding() }
List{ ForEach(global.myArray, id: \.self)
{ i in Text(i).padding()} }
} //end VStack
} //end View
} //end ContentView
Ok so my content view is this
import SwiftUI
struct ContentView: View {
#ObservedObject private var user = User()
var body: some View {
VStack {
ShowName()
TextField("First name", text: $user.firstName)
TextField("Last name", text: $user.lastName)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I migrated my class to swift file
import Foundation
class User: ObservableObject {
#Published var firstName = "Bilbo"
#Published var lastName = "Baggins"
}
Then I moved the single view into an external view file
import SwiftUI
struct ShowName: View {
#ObservedObject private var user = User()
var body: some View {
Text("Your name is \(user.firstName) \(user.lastName)")
.padding()
}
}
struct ShowName_Previews: PreviewProvider {
static var previews: some View {
Testing()
}
}
I have Observed the same class but it does not update, when the class and view were in the
Is there something I need to do special in the ShowName() view to make it observe the class, or am I missing the point entirely.
When the main view bound to the class updates the class, the ShowName view is not updated, I know this is me not understanding it but its so hard to ask Google with something like this, so please be kind, im 47 and learning a new language ;)
Sorry ive only been doing swift a few days migrating from Angular/c++/PHP
In ContentView Change
#StateObject private var user = User()
And
ShowName(user: user)
Then in ShowName remove the initializer
#ObservedObject var user = User
Every time you initialize User() you get a different instance one does not know what the other is doing.
The change to state object is because it is unsafe to use #ObservedObject like that.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
That is because the instance of User is unique in both of those views. To correct it, you would want to pass in the user to the ShowName view instead of create a new one in the view itself. It would look something like this:
struct ShowName: View {
#ObservedObject private var use: User
init(_ user: User) {
self.user = user
}
var body: some View {
Text("Your name is \(user.firstName) \(user.lastName)")
.padding()
}
}
Then in content view you would initialize it like:
ShowName(user)
The important thing to realize is that every time you write User() a new user instance is created. Where this change would pass the same user from content view to the show name view.
I’m writing this from my phone so something may be a little off.
I have the following 2 structs within separate files and displayed in the contentView. What I'm trying to understand is how to maintain the contentView as only displaying and organizing the UI. Placing all of my other views in separate files. My first thought was the correct approach would be to use static variables updated by functions that are called from the button press action. But the buttons text did not update accordingly. As they are dynamically updated according to #State.
update:
I attempted to solve this by using protocols and delegates to no avail. By my understanding this delegate call should be receiving on the other end and updating structcop.ID and the change should be reflected in the content view.
FILE 1
import SwiftUI
struct structdispatch: View {
var radio:RadioDelegate?
func send() {
radio?.update()
self.debug()
}
var body: some View {
Button(action: self.send)
{Text("DISPATCHER")}
}
func debug() {
print("Button is sending?")
}
}
struct structdispatch_Previews: PreviewProvider {
static var previews: some View {
structdispatch()
}
}
**FILE 2:**
import SwiftUI
protocol RadioDelegate {
func update()
}
struct structcop: View, RadioDelegate {
#State public var ID:Int = 3
func update(){
print("message recieved")
self.ID += 1
print(self.ID)
}
var body: some View {
Text(String(self.ID))
}
}
struct structcop_Previews: PreviewProvider {
static var previews: some View {
structcop()
}
}
DEBUG CONSOLE RETURNS:
The Button is working
View is updated on some internal DynamicProperty change, like #State, so here is possible solution
Tested with Xcode 12 / iOS 14
struct structcop: View {
static public var ID = 3
#State private var localID = Self.ID {
didSet {
Self.ID = localID
}
}
var body: some View {
Button(action: printme)
{Text(String(localID))}
}
func printme(){
self.localID = 5
print(structcop.ID)
}
}
Solution:
After some digging I have a working solution but I'm still curious if there is a way to modify properties of other structs while maintaining dynamic view updates.
Solution: store data for display in an observable object which will either read or act as the model which the user is interacting with.
An observable object is a custom object for your data that can be bound to a view from storage in SwiftUI’s environment. SwiftUI watches for any changes to observable objects that could affect a view, and displays the correct version of the view after a change. -apple
A new model type is declared that conforms to the ObservableObject protocol from the Combine framework. SwiftUI subscribes to the observable object and updates relevant views that need refreshing when the data changes. SceneDelegate.swift needs to have the .environmentObject(_:) modifier added to your root view.
Properties declared within the ObservableObject should be set to #Published so that any changes are picked up by subscribers.
For test code I created an ObservableObject called headquarters
import SwiftUI
import Combine
final class hq: ObservableObject {
#Published var info = headQuarters
}
let headQuarters = hqData(id: 3)
struct hqData {
var id: Int
mutating func bump() {
self.id += 1
}
}
In my struct dispatch I subscribed to the object and called a function that iterated the id in the model whenever the button was pressed. My struct cop also subscribed to the object and thus the model and button text updated accordingly to changes.
struct dispatch: View {
#EnvironmentObject private var hq: headqarters
var body: some View {
Button(action: {self.hq.info.bump()}) {
Text("Button")
}
}
}
struct cop: View {
#EnvironmentObject private var hq: headquarters
var body: some View {
Text(String(self.hq.info.id))
}
}