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

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)

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

Swift & SwiftUI - Conditional global var

I want to make a global variable in Swift, so that its Data is accessible to any view that needs it. Eventually it will be a var so that I can mutate it, but while trying to get past this hurdle I'm just using it as let
I can do that by putting this as the top of a file (seemingly any file, Swift is weird):
let myData: [MyStruct] = load("myDataFile.json)
load() returns a JSONDecoder(). MyStruct is a :Hashable, Codable, Identifiable struct
That data is then available to any view that wants it, which is great. However, I want to be able to specify the file that is loaded based on a condition - I'm open to suggestions, but I've been using an #AppStorage variable to determine things when inside a View.
What I'd like to do, but can't, is do something like:
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
if(appStorageVar == "Condition2") {
let myData: [MyStruct] = load("myDataFile2.json")
}
else {
let myData: [MyStruct] = load("myDataFile.json")
}
I can do this inside a View's body, but then it's only accessible to that View and then I have to repeat it constantly, which can't possibly the correct way to do it.
You could change just change the global in an onChange on the AppStorage variable. This is an answer to your question, but you have the problem that no view is going to be updating itself when the global changes.
var myData: [MyStruct] = load("myDataFile.json)
struct ContentView: View {
#AppStorage("appStorageVar") var appStorageVar: String = "Condition1"
var body: some View {
Button("Change value") {
appStorageVar = "Condition2"
}
.onChange(of: appStorageVar) { newValue in
myData = load(newValue == "Condition1" ? "myDataFile.json" : "myDataFile2.json")
}
}
}

Swift: ObservableObject, initializer doesn't define all properties?

Currently I have the code below which I'm trying to use as a navigation switch so I can navigate through different views without using the crappy NavigationLinks and otherwise. I'm by default a WebDev, so I've been having a mountain of issues transferring my knowledge over to Swift, the syntax feels completely dissimilar to any code I've written before. Anyways, here's the code;
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange: PassthroughSubject<ViewRouter,Never>
#Published var currentPage: String = "page1" {
didSet {
objectWillChange.send(self)
}
}
init(currentPage: String) {
self.currentPage = currentPage
}
}
As you can see, it's really simple, and I just use the object to switch values and display different views on another file, the only errors which prevent me from building it is the fact that the initializer is saying "Return from initializer without initializing all stored properties", even though the only variable is the currentPage variable which is defined. I know it's saying that objectWillChange is not defined by the message, but objectWillChange doesn't have any value to be assigned. Any help would be appreciated.
You just declare objectWillChange, but don't initialise it.
Simply change the declaration from
let objectWillChange: PassthroughSubject<ViewRouter,Never>
to
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
However, using a PassthroughSubject shouldn't be necessary. currentPage is already #Published, so you can simply subscribe to its publisher. What you are trying to achieve using a PassthroughSubject and didSet is already defined by the swiftUI property wrappers, ObservableObject and Published.
class ViewRouter: ObservableObject {
#Published var currentPage: String
init(currentPage: String) {
self.currentPage = currentPage
}
}
Then you can simply do
let router = ViewRouter(currentPage: "a")
router.$currentPage.sink { page in print(page) }
router.currentPage = "b" // the above subscription prints `"b"`

SwiftUI not being updated with manual publish

I have a class, a “clock face” with regular updates; it should display an array of metrics that change over time.
Because I’d like the clock to also be displayed in a widget, I’ve found that I had to put the class into a framework (perhaps there’s another way, but I’m too far down the road now). This appears to have caused a problem with SwiftUI and observable objects.
In my View I have:
#ObservedObject var clockFace: myClock
In the clock face I have:
class myClock: ObservableObject, Identifiable {
var id: Int
#Publish public var metric:[metricObject] = []
....
// at some point the array is mutated and the display updates
}
I don’t know if Identifiable is needed but it’s doesn’t make any difference to the outcome. The public is demanded by the compiler, but it’s always been like that anyway.
With these lines I get a runtime error as the app starts:
objc[31175] no class for metaclass
So I took off the #Published and changed to a manual update:
public var metric:[metricObject] = [] {
didSet {
self.objectWillChange.send()`
}
}
And now I get a display and by setting a breakpoint I can see the send() is being called at regular intervals. But the display won’t update unless I add/remove from the array. I’m guessing the computed variables (which make up the bulk of the metricObject change isn’t being seen by SwiftUI. I’ve subsequently tried adding a “dummy” Int to the myClock class and setting that to a random value to trying to trigger a manual refresh via a send() on it’s didSet with no luck.
So how can I force a periodic redraw of the display?
What is MetricObject and can you make it a struct so you get Equatable for free?
When I do this with an Int it works:
class PeriodicUpdater: ObservableObject {
#Published var time = 0
var subscriptions = Set<AnyCancellable>()
init() {
Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.time = self.time + 1
})
.store(in: &subscriptions)
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Text("\(self.updater.time)")
}
}
So it's taken a while but I've finally got it working. The problem seemed to be two-fold.
I had a class defined in my framework which controls the SwiftUI file. This class is sub-classed in both the main app and the widget.
Firstly I couldn't use #Published in the main class within the framework. That seemed to cause the error:
objc[31175] no class for metaclass
So I used #JoshHomman's idea of an iVar that's periodically updated but that didn't quite work for me. With my SwiftUI file, I had:
struct FRMWRKShape: Shape {
func drawShape(in rect: CGRect) -> Path {
// draw and return a shape
}
}
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
FRMWRKShape()
//....
FRMWRKShape() //slightly different parameters are passed in
}
}
The ContentView was executed every second as I wanted, however the FRMWRKShape code was called but not executed(?!) - except on first starting up - so the view doesn't update. When I changed to something far less D.R.Y. such as:
struct ContentView: View {
#ObservedObject var updater = PeriodicUpdater()
var body: some View {
Path { path in
// same code as was in FRMWRKShape()
}
//....
Path { path in
// same code as was in FRMWRKShape()
// but slightly different parameters
}
}
}
Magically, the View was updated as I wanted it to be. I don't know if this is expected behaviour, perhaps someone can say whether I should file a Radar....

A #State static property is being reinitiated without notice

I have a view which looks like this:
struct Login: View {
#State static var errorMessage = ""
init() {
// ...
}
var body: some View {
// ...
}
}
I set errorMessage as static so I can set an error message from anywhere.
The problem is that even being static, it is always reinitiated each time the login view is showed, so the error message is always empty. I was thinking that maybe the presence of the init() method initiate it somehow, but I didn't figure how to fix this. What can I do?
I set errorMessage as static so I can set an error message from anywhere.
This is a misunderstanding of #State. The point of #State variables is to manage internal state to a View. If something external is even looking at a #State variable, let alone trying to set it, something is wrong.
Instead, what you need is an #ObservableObject that is passed to the view (or is accessed as a shared instance). For example:
class ErrorManager: ObservableObject {
#Published var errorMessage: String = "xyz"
}
This is the global thing that manages the error message. Anyone can call errorManager.errorMessage = "something" to set it. You can of course make this a shared instance if you wanted by adding a property:
static let shared = ErrorManager()
With that, you then pass it to the View:
struct Login: View {
#ObservedObject var errorManager: ErrorManager
var body: some View {
Text(errorManager.errorMessage)
}
}
Alternately, you could use the shared instance if you wanted:
#ObservedObject var errorManager = ErrorManager.shared
And that's it. Now change to the error automatically propagate. It's more likely that you want a LoginManager or something like that to handle the whole login process, and then observe that instead, but the process is the same.
#State creates a stateful container that is associated with an instance of a view. Each instance of your view has it's own copy of that #State container.
In contrast, a static variable does not change across instances.
These two concepts are not compatible. You should not be using static with #State.