Accessing Viewmodel Field in Swiftui Using Xcode 12: "Accessing State's Value Outside of Being Installed on a View"

Accessing ViewModel field in SwiftUI using Xcode 12: Accessing State's value outside of being installed on a View

Thanks to @Andrew's answer I figured out how to make it work again. First you change the @State to @Published:

    @Published public var amount: Int = 1

Next, you need to change how your Picker is bound to the data:

            Picker(selection: $model.amount, label: Text("Amount")) {
Text("€1").tag(1)
Text("€2").tag(2)
Text("€5").tag(5)
Text("€10").tag(10)
}.pickerStyle(SegmentedPickerStyle())

So we went from model.$amount to $model.amount.

SwiftUI binding bool outside view - 'Accessing State's value outside of being installed on a View'

You can try having all the logic in the ViewModel:

class ViewModel: ObservableObject {
@Published var currentQuestion: String!
@Published var showResults = false

private var questions = ["one", "two", "three"]
private var index = 0

init() {
currentQuestion = questions[index]
}

func nextQuestion() {
if index < questions.count {
index += 1
currentQuestion = questions[index]
} else {
showResults = true
}
}
}

and UI components (eg. buttons) in the View:

struct ContentView: View {
@ObservedObject var viewModel = ViewModel()

var body: some View {
NavigationView {
VStack {
Text(viewModel.currentQuestion)
Button(action: viewModel.nextQuestion) {
Text("Next")
}
}
// if viewModel.showResults == true -> navigate to results
.background(NavigationLink(destination: ResultsView(), isActive: $viewModel.showResults) { EmptyView() } .hidden())
}
}
}

In the above example you go to the ResultsView using a NavigationLink but you can easily replace the current view with results. The presentation is up to you.

It is possible to accessing FocusState's value outside of the body of a View

It is in-view wrapper (same as State). But it is possible to map it to published property like in below approach.

Tested with Xcode 13.2 / iOS 15.2

struct ContentView: View {
@ObservedObject private var viewModel = ViewModel()
@FocusState var hasFocus: Bool

var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
.onChange(of: viewModel.hasFocus) {
hasFocus = $0
}
.onChange(of: hasFocus) {
viewModel.hasFocus = $0
}
Button("Set Focus") {
viewModel.hasFocus = true
}
}
}
}

class ViewModel: ObservableObject {
@Published var textField: String = ""
@Published var hasFocus: Bool = false
}

Accessing StateObject's object without being installed on a View. This will create a new instance each time - SwiftUI

You cannot access any value before they get initialized, use onAppear():

import SwiftUI

@main
struct YourApp: App {

@StateObject private var amplifyConfig: AmplifyConfig = AmplifyConfig()

var body: some Scene {

WindowGroup {
ContentView()
.onAppear() {
if (!amplifyConfig.isAmplifyConfigured) {
amplifyConfig.dataStoreHubEventSubscriber()
amplifyConfig.configureAmplify()
}
}
}
}
}


Update: An actual use case

import SwiftUI

@main
struct YourApp: App {

@StateObject private var amplifyConfig: AmplifyConfig = AmplifyConfig()

@State private var isLoaded: Bool = Bool()

var body: some Scene {

WindowGroup {

VStack {
if (isLoaded) { ContentView() }
else { Text("Loading . . .") }
}
.onAppear() {
if (!amplifyConfig.isAmplifyConfigured) {
amplifyConfig.dataStoreHubEventSubscriber()
amplifyConfig.configureAmplify()
completionHandler { value in isLoaded = value }
}
else {
isLoaded = true
}
}
}
}
}

func completionHandler(value: @escaping (Bool) -> Void) {

// Some heavy work here, I am using DispatchQueue.main.asyncAfter for replicating that heavy work is happening! But you use your own code here.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(3000)) { value(true) }

}

Accessing state variable in SwiftUI View outside the body

The error message is pretty much saying exactly what the problem is. However, although you cannot access State variables outside the body function (or a function called from body), you can access observable objects. So simply changing @State by @ObservedObject and defining a tiny class, is enough to make it work.

Note that this solution is to keep your workflow idea intact. Although, there're other ways to accomplish the same thing.

import SwiftUI
import Combine

class Model: ObservableObject {
@Published var keyboardHeight: CGFloat = 0
}

struct ContentView: View {
@ObservedObject var model = Model()

var cancellables: Set<AnyCancellable> = []

init() {
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
.merge(with: NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification))
.compactMap({ notification in
guard let keyboardFrameValue: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return nil }

let keyboardFrame = keyboardFrameValue.cgRectValue

if keyboardFrame.origin.y == UIScreen.main.bounds.height {
return 0
} else {
return keyboardFrame.height - (UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0)
}
})
.assign(to: \.keyboardHeight, on: model)
.store(in: &cancellables)
}

var body: some View {
VStack{
ZStack(alignment: Alignment.bottom) {
List {
Text("Default text").foregroundColor(Color.red)
}
TextField("Placeholder", text: .constant(""))
.frame(minHeight: 30)
.cornerRadius(8.0)
.padding(10)
.background(Color.blue)
}
Spacer()
.frame(height: model.keyboardHeight)
}
}
}

How to assign value to @State in View from ViewModel?

you could try this approach, using .onReceive(...). Add this to your
ZStack or NavigationView:

 .onReceive(Just(viewModel.moreRemaining)) { val in
reachedLastPage = !val
}

Also add: import Combine

SwiftUI: Change view @State property from ViewModel

You can directly bind to the @Published property with just:

$viewModel.startTime

Which will be of type Binding<Date>, even though we declare it as just a @Published property on BookTimeViewModel.

Example code:

class BookTimeViewModel: ObservableObject {
@Published var startTime: Date = .now
}
struct BookTimeView: View {
@ObservedObject var viewModel: BookTimeViewModel

var body: some View {
DatePicker("pick time", selection: $viewModel.startTime, displayedComponents: .hourAndMinute)
}
}

Similar to my answer here.



Related Topics



Leave a reply



Submit