How to delay setting value of #State variable in SwiftUI - swift

I want to create an application with a date picker. The initial value of the date picker (startDate) requires some computation performed in another class. (Let's say for example that this date is initially set to a customer's enrolment date in a service, which is retrieved from a database.) The user can leave that date as-is, or select a new date using the date picker.
I haven't been able to find a way of setting this initial value. The following simplified code illustrates the issue. The date picker shows the initial value of today's date rather than the desired date (here, Date.distantPast.) The date picker otherwise works fine.
Things I've tried:
If I set the value in the init clause (as shown below), it is ignored (no error reported, but the value is not written to startDate variable).
Trying to set the value of startDate inline with its declaration gives a compile-time error message ("Cannot use instance member 'db' within property initializer; property initializers run before 'self' is available".)
Wrapping the DataPicker in an if-statement so it won't not be displayed until the startDate has been computed makes no difference.
Is there any way to defer the setting of the initial date selection in a date picker until it can be computed?
import SwiftUI
struct ContentView: View {
private let db = DB()
#State private var startDate = Date()
var body: some View {
DatePicker("Start Date",
selection: $startDate,
displayedComponents: [.date])
}
init() {
startDate = db.theDate
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class DB {
var theDate: Date
init() {
theDate = Date.distantPast
}
}

You can create a ViewModel to match your view. In the ViewModel you have more possibilities. For example, you can easily initialise date in the Init.
This would look something like this:
class ContentViewModel: ObservableObject {
private let db = DB()
#Published var date: Date
init() {
date = db.theDate
}
}
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
DatePicker("Start Date",
selection: $viewModel.date,
displayedComponents: [.date])
}
}
Make sure that the ViewModel is an ObservableObject and that properties that can change use the property wrapper #Published.
If your app no longer needs to support iOS 13, you can also use #StateObject instead of #ObservedObject.

Related

SwiftUI: Set a Published value in an ObservableObject from the UI (Picker, etc.)

Update:
This question is already solved (see responses below). The correct way to do this is to get your Binding by projecting the
ObservableObject For example, $options.refreshRate.
TLDR version:
How do I get a SwiftUI Picker (or other API that relies on a local Binding) to immediately update my ObservedObject/EnvironmentObject. Here is more context...
The scenario:
Here is something I consistently need to do in every SwiftUI app I create...
I always make some class that stores any user preference (let's call this class Options and I make it an ObservableObject.
Any setting that needs to be consumed is marked with #Published
Any view that consumes this brings it in as a #ObservedObject or #EnvironmentObject and subscribes to changes.
This all works quite nicely. The trouble I always face is how to set this from the UI. From the UI, here is usually what I'm doing (and this should all sound quite normal):
I have some SwiftUI view like OptionsPanel that drives the Options class above and allows the user to choose their options.
Let's say we have some option defined by an enum:
enum RefreshRate {
case low, medium, high
}
Naturally, I'd choose a Picker in SwiftUI to set this... and the Picker API requires that my selection param be a Binding. This is where I find the issue...
The issue:
To make the Picker work, I usually have some local Binding that is used for this purpose. But, ultimately, I don't care about that local value. What I care about is immediately and instantaneously broadcasting that new value to the rest of the app. The moment I select a new refresh rate, I'd like immediately know that instant about the change. The ObservableObject (the Options class) object does this quite nicely. But, I'm just updating a local Binding. What I need to figure out is how to immediately translate the Picker's state to the ObservableObject every time it's changed.
I have a solution that works... but I don't like it. Here is my non-ideal solution:
The non-ideal solution:
The first part of the solution is quite actually fine, but runs into a snag...
Within my SwiftUI view, rather than do the simplest way to set a Binding with #State I can use an alternate initializer...
// Rather than this...
#ObservedObject var options: Options
#State var refreshRate: RefreshRate = .medium
// Do this...
#ObservedObject var options: Options
var refreshRate: Binding<RefreshRate>(
get: { self.options.refreshRate },
set: { self.options.refreshRate = $0 }
)
So far, this is great (in theory)! Now, my local Binding is directly linked to the ObservableObject. All changes to the Picker are immediately broadcast to the entire app.
But this doesn't actually work. And this is where I have to do something very messy and non-ideal to get it to work.
The code above produces the following error:
Cannot use instance member 'options' within property initializer; property initializers run before 'self' is available
Here my my (bad) workaround. It works, but it's awful...
The Options class provides a shared instance as a static property. So, in my options panel view, I do this:
#ObservedObject var options: Options = .shared // <-- This is still needed to tell SwiftUI to listen for updates
var refreshRate: Binding<RefreshRate>(
get: { Options.shared.refreshRate },
set: { Options.shared.refreshRate = $0 }
)
In practice, this actually kinda works in this case. I don't really need to have multiple instances... just that one. So, as long as I always reference that shared instance, everything works. But it doesn't feel well architected.
So... does anyone have a better solution? This seems like a scenario EVERY app on the face of the planet has to tackle, so it seems like someone must have a better way.
(I am aware some use an .onDisapear to sync local state to the ObservedObject but this isn't ideal either. This is non-ideal because I value having immediate updates for the rest of the app.)
The good news is you're trying way, way, way too hard.
The ObservedObject property wrapper can create this Binding for you. All you need to say is $options.refreshRate.
Here's a test playground for you to try out:
import SwiftUI
enum RefreshRate {
case low, medium, high
}
class Options: ObservableObject {
#Published var refreshRate = RefreshRate.medium
}
struct RefreshRateEditor: View {
#ObservedObject var options: Options
var body: some View {
// vvvvvvvvvvvvvvvvvvvv
Picker("Refresh Rate", selection: $options.refreshRate) {
// ^^^^^^^^^^^^^^^^^^^^
Text("Low").tag(RefreshRate.low)
Text("Medium").tag(RefreshRate.medium)
Text("High").tag(RefreshRate.high)
}
.pickerStyle(.segmented)
}
}
struct ContentView: View {
#StateObject var options = Options()
var body: some View {
VStack {
RefreshRateEditor(options: options)
Text("Refresh rate: \(options.refreshRate)" as String)
}
.padding()
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(ContentView())
It's also worth noting that if you want to create a custom Binding, the code you wrote almost works. Just change it to be a computed property instead of a stored property:
var refreshRate: Binding<RefreshRate> {
.init(
get: { self.options.refreshRate },
set: { self.options.refreshRate = $0 }
)
}
If I understand your question correctly, you want
to Set a Published value in an ObservableObject from the UI (Picker, etc.) in SwiftUI.
There are many ways to do that, I suggest you use a ObservableObject class, and use it directly wherever you need a binding in a view, such as in a Picker.
The following example code shows one way of setting up your code to do that:
import Foundation
import SwiftUI
// declare your ObservableObject class
class Options: ObservableObject {
#Published var name = "Mickey"
}
struct ContentView: View {
#StateObject var optionModel = Options() // <-- initialise the model
let selectionSet = ["Mickey", "Mouse", "Goofy", "Donald"]
#State var showSheet = false
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.red)
Picker("names", selection: $optionModel.name) { // <-- use the model directly as a $binding
ForEach (selectionSet, id: \.self) { value in
Text(value).tag(value)
}
}
Button("Show other view") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
SheetView(optionModel: optionModel) // <-- pass the model to other view, see also #EnvironmentObject
}
}
}
struct SheetView: View {
#ObservedObject var optionModel: Options // <-- receive the model
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.green) // <-- show updated value
}
}
}
If you really want to have a "useless" intermediate local variable, then use this approach:
struct ContentView: View {
#StateObject var optionModel = Options() // <-- initialise the model
let selectionSet = ["Mickey", "Mouse", "Goofy", "Donald"]
#State var showSheet = false
#State var localVar = "" // <-- the local var
var body: some View {
VStack {
Text(optionModel.name).foregroundColor(.red)
Picker("names", selection: $localVar) { // <-- using the localVar
ForEach (selectionSet, id: \.self) { value in
Text(value).tag(value)
}
}
.onChange(of: localVar) { newValue in
optionModel.name = newValue // <-- update the model
}
Button("Show other view") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
SheetView(optionModel: optionModel) // <-- pass the model to other view, see also #EnvironmentObject
}
}
}

Use an object to collect form data and set default value in Swift

I'm brand new in Swift development, and I can't for the life of me figure this out. All I want to do is use an object to collect a forms data, save it to Realm, and then send it to the server via API.
In every example I've found, people are creating a new State variable for each element in their form. This seems unpractical for forms with many fields, so I tried to just create an object with properties that match the form fields I need. If I don't try to set any default values, this works as I expect. But when I try to set some default values for the form in the init(), I get errors that I don't know how to resolve. Here's some partial code:
The object that will be used to collect the form data:
class RecordObject: ObservableObject {
var routeId: Int?
var typeId: Int?
var inDate: Date?
var outDate: Date?
var nextDate: Date?
// .... more properties
}
And what I want to do is in the View, set some default values in the init() that need some logic to derive the value:
In the View:
struct AddLauncherView: View {
var route: Route // this is passed from a previous view that lists all the routes
#StateObject var record: RecordObject = RecordObject() // this is where I want to store all the form data
init(){
_record.routeId = self.route.id // I get the error: Referencing property 'routeId' requires wrapped value of type 'RecordObject'
self.record.inDate = Date() // this gives the error: 'self' used before all stored properties are initialized
//self.record.nextDate = here I need to do some date math to figure out the next date
}
var body: some View {
Form{
DatePicker("In Date", selection: $record.inDate, displayedComponents: .date)
// .... more form elements
}
}
}
I know I could add default values in the RecordObject class, but there are some properties that will need some logic to assign the default value.
Can someone help out a Swift noob and give me some pointers for making this work? Do I really need to create a State var for each form field in the View?
If you did use a class (ObservableObject), you'd want the properties to be annotated with #Published. However, it's probably a better idea to use a struct with a #State variable that contains all of the various properties you need. That may look like this:
struct Record {
var routeId: Int?
var typeId: Int?
var inDate: Date?
var outDate: Date?
var nextDate: Date?
}
struct AddLauncherView: View {
var route: Route
#State var record: Record
init(route: Route) {
self.route = route
_record = State(initialValue: Record(routeId: route.id,
typeId: nil,
inDate: Date(),
outDate: nil,
nextDate: nil))
}
var body: some View {
Form{
DatePicker("In Date",
selection: Binding<Date>(get: {record.inDate ?? Date()},
//custom binding only necessary if inDate is Date? -- if you make it just Date, you can bind directly to $record.inDate
set: {record.inDate = $0}),
displayedComponents: .date)
// .... more form elements
}
}
}
You are using the ObservableObject incorrectly. You data model should be a struct, and then the class publishes values that are of the type of the struct, so:
struct Record: Identifiable {
// First, save yourself some grief later and make it conform to Identifiable
let id = UUID()
// The listed variables are probably non-optional in your data model.
// If they are optional, mark them as optional, otherwise
var routeId: Int
var typeId: Int
var inDate: Date
// These are more likely optional
var outDate: Date?
var nextDate: Date?
// .... more properties
}
class RecordObject: ObservableObject {
// Make the source of truth #Published so things are updated in your view
#Published var records: [Record] = []
// OR use an init
#Published var records: [Record]
init(records: [Records] {
self.records = records
// or call some function that imports/creates the records...
}
}

SwiftUI View not updating with binding to computed property on ObservableObject

I have a very simple SwiftUI view that only shows a TextField. The text field's text is bound to the string property of my viewModel that I instantiate as a #StateObject:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
TextField("Placeholder", text: $viewModel.string)
.padding()
}
}
The one thing that's "out of the ordinary" is that my string property is not a #Published property, but a simple computed property. In its setter, it sets the displayedString property to a fixed string (for testing purposes) which is a #Published property:
class ViewModel: ObservableObject {
#Published var displayedString: String = ""
var string: String {
get { displayedString }
set { displayedString = "OVERRIDE" }
}
}
So when I type a string into the text field, I would expect the setter to trigger a view update (as it updates a #Published property) and then in the view, the text field should be updated with the displayedString. So in other words: No matter what I type, the text field should always show the string OVERRIDE.
But that's not the case!
It works for the very first letter I type, but then I can freely type anything into the text field.
For example, if I launch the app with only this view and type the string
123456789
this is what is displayed on the text field:
OVERRIDE23456789
Why is that?
And how can I get the text field to always be up-to-date to what's set on the viewModel? (I know, I can just hook the text field to a #Published property directly, but I'm doing this computed property stuff for a reason and want to understand why it doesn't work as expected.)
Additional Observation:
When I add a Text label above the TextField and just make it show the viewModel.string, it shows the correct (expected) string:
var body: some View {
Text(viewModel.string)
TextField("Placeholder", text: $viewModel.string)
.padding()
}
I’m not sure but I think a custom binding might work for you
In the ViewModel use
var string: Binding<String> { Binding(
get: { displayedString },
set: { displayedString = “OVERRIDE” } )}
Then in body use: viewModel.string instead of $viewModel.string in the TextField and use: viewModel.string.wrappedValue in the text view
Stuf

SwiftUI: Pass value to and use it in init of child view

I've been trying to create a small calendar app with SwiftUI and ran into some issues while trying to pass a value to a child view and use it in its init.
My code looks like this:
ContentView (parent view):
struct ContentView: View {
#State var selectedMonth: Date
var body: some View {
MonthGridView(selectedMonth: $selectedMonth)
}
}
MonthGridView (child view):
struct MonthGridView: View {
#Binding private var selectedMonth: Date
var days: [Int]
//this is the part where I'm having troubles
init(selectedMonth: Binding<Date>) {
self._selectedMonth = selectedMonth
days = dayIndices(currentMonth: $selectedMonth) //custom function, also in this line is the error right now
}
var body: some View {
//code
}
}
I have looked through a lot of posts on here and a wide variety of tutorials and this is what I came up with. I've tried moving some code around, but wasn't able to get it fully working. I imagine the problem is somewhere around the init, maybe about the Binding wrapper, but I was unable to find information about how to unwrap it.
Appreciate any help getting this working.
It'll be easier to understand the problem if we “de-sugar” the #Binding property wrapper. When you say this:
#Binding private var selectedMonth: Date
Swift translates that into this:
private var _selectedMonth: Binding<Date>
private var $selectedMonth: Date { _selectedMonth.projectedValue }
private var selectedDate: Date {
get { _selectedMonth.wrappedValue }
nonmutating set { _selectedMonth.wrappedValue }
}
Here is your init again:
init(selectedMonth: Binding<Date>) {
self._selectedMonth = selectedMonth
days = dayIndices(currentMonth: $selectedMonth) //custom function, also in this line is the error right now
}
You're using $selectedMonth before days has been initialized. But as I showed above, $selectedMonth is a computed property. You are not allowed to call any methods on self before self is fully initialized, and the getter of a computed property counts as a method.
In general, you can work around the limitation by accessing _selectedMonth.projectedValue directly:
days = dayIndices(currentMonth: _selectedMonth.projectedValue)
However, in this case, all of _selectedMonth, _selectedMonth.projectedValue, and the init parameter selectedMonth are the same Binding, you can use any of them directly. So either of these will also work:
days = dayIndices(currentMonth: selectedMonth)
days = dayIndices(currentMonth: _selectedMonth)

How to update global variable with TextField?

I have some variables that I need in several views and I want the user to be able to change these. These variables edit a link, so depending on for example what the longitude is the result is different. And the values I get from the link are displayed in a graph.
So I want the user to be able to enter their own value for the given variables(in the code below). I don't care if it is in the same view, or if consists of two views and the variables are passed to the view with the graphs.
The only important thing, is that the graph and the link refreshes when the user clicks a button, so that the right graph is displayed.
Another important thing, is that I need to be able to access the variables at top-level. This is my code until now, but it shows the error:
Cannot convert value of type 'string' to expected argument type 'Binding'
import SwiftUI
var month = "1"
var day = "1"
var year = "2020"
var latitude = "59.911232"
var longitude = "10.757933"
var offset = "1.0"
struct ContentView: View {
var body: some View {
GraphView()
TextField(text: latitude)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
TextField requires binding to some dynamic property source of truth. If you want to keep those values globally then wrap them into some shared instance of view model, like
final class Params: ObservableObject {
static let global = Params()
var month = "1"
var day = "1"
var year = "2020"
var latitude = "59.911232"
var longitude = "10.757933"
var offset = "1.0"
}
so now we are able to observe (and update, and bind to) those parameters from view
struct ContentView: View {
#ObservedObject var global = Params.global
var body: some View {
GraphView()
TextField(text: $global.latitude)
}
}