How do I pass a property from an ObservedObject
in a View
, to another class's initializer in the same View
? I get an error with my ObservedObject
:
Cannot use instance member 'project' within property initializer; property initializers run before 'self' is available
The reason I want to do this is I have a class which has properties that depend on a value from the ObservedObject
.
For example, I have an ObservedObject
called project
. I want to use the property, project.totalWordsWritten
, to change the session
class's property, session.totalWordCountWithSession
:
struct SessionView: View {
@Binding var isPresented: Bool
@ObservedObject var project: Project
// How to pass in project.totalWordsWritten from ObservedObject project to totalWordCount?
@StateObject var session:Session = Session(startDate: Date(), sessionWordCount: 300, totalWordCount: 4000)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Form {
Section {
Text("Count")
HStack {
Text("Session word count")
TextField("", value: $session.sessionWordCount, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
}
HStack {
// Changing text field here should change the session count above
Text("Total word count")
TextField("", value: $session.totalWordCountWithSession, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
}
}
}
}.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// Save this session into the project
project.addSession(newSession: session)
isPresented = false
}
}
}
}
}
}
struct SessionView_Previews: PreviewProvider {
static var previews: some View {
SessionView(isPresented: .constant(true), project: Project(title: "TestProject", startWordCount: 0))
}
}
Below is the rest of the example:
HomeView.swift
import SwiftUI
struct HomeView: View {
@State private var showingSessionPopover:Bool = false
@StateObject var projectItem:Project = Project(title: "Test Project", startWordCount: 4000)
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text(projectItem.title).font(Font.custom("OpenSans-Regular", size: 18))
.fontWeight(.bold)
Text("Count today: \(projectItem.wordsWrittenToday)")
Text("Total: \(projectItem.totalWordsWritten)")
}
.toolbar {
ToolbarItem {
Button(action: {
showingSessionPopover = true
}, label: {
Image(systemName: "calendar").imageScale(.large)
}
)
}
}
}.popover(isPresented: $showingSessionPopover) {
SessionView(isPresented: $showingSessionPopover, project: projectItem)
}
}
}
Session.swift:
import Foundation
import SwiftUI
class Session: Identifiable, ObservableObject {
init(startDate:Date, sessionWordCount:Int, totalWordCount: Int) {
self.startDate = startDate
self.endDate = Calendar.current.date(byAdding: .minute, value: 30, to: startDate) ?? Date()
self.sessionWordCount = sessionWordCount
self.totalWordCount = totalWordCount
self.totalWordCountWithSession = self.totalWordCount + sessionWordCount
}
var id: UUID = UUID()
@Published var startDate:Date
@Published var endDate:Date
var totalWordCount: Int
var sessionWordCount:Int
@Published var totalWordCountWithSession:Int {
didSet {
sessionWordCount = totalWordCountWithSession - totalWordCount
}
}
}
Project.swift
import SwiftUI
class Project: Identifiable, ObservableObject {
var id: UUID = UUID()
@Published var title:String
var sessions:[Session] = []
@Published var wordsWrittenToday:Int = 0
@Published var totalWordsWritten:Int = 0
@Published var startWordCount:Int
init(title:String,startWordCount:Int) {
self.title = title
self.startWordCount = startWordCount
self.calculateDailyAndTotalWritten()
}
// Create a new session
func addSession(newSession:Session) {
sessions.append(newSession)
calculateDailyAndTotalWritten()
}
// Re-calculate how many
// today and in total for the project
func calculateDailyAndTotalWritten() {
wordsWrittenToday = 0
totalWordsWritten = startWordCount
for session in sessions {
if (Calendar.current.isDateInToday(session.startDate)) {
wordsWrittenToday += session.sessionWordCount
}
totalWordsWritten += session.sessionWordCount
}
}
}
You can use the StateObject
initializer in init
:
struct SessionView: View {
@Binding var isPresented: Bool
@ObservedObject var project: Project
@StateObject var session:Session = Session(startDate: Date(), sessionWordCount: 300, totalWordCount: 4000)
init(isPresented: Binding<Bool>, project: Project, session: Session) {
_isPresented = isPresented
_session = StateObject(wrappedValue: Session(startDate: Date(), sessionWordCount: 300, totalWordCount: project.totalWordsWritten))
self.project = project
}
var body: some View {
Text("Hello, world")
}
}
Note that the documentation says:
You don’t call this initializer directly
But, it has been confirmed by SwiftUI engineers in WWDC labs that this is a legitimate technique. What runs in wrappedValue
is an autoclosure
and only runs on the first init of StateObject, so you don't have to be concerned that every time your View
updates that it will run.
In general, though, it's a good idea to try to avoid doing things in the View
's init
. You could consider instead, for example, using something like task
or onAppear
to set the value and just put a placeholder value in at first.