SwiftUI Alert action requires two taps - swiftui

The desired action in the following alert (in a SwiftUI View) only runs after the primaryButton ("Yes") is tapped a second time (on the second appearance of the alert):
.alert(isPresented: $viewModel.showingAlert) {
Alert(
title: Text("Confirm your Selection"),
message: Text("Are you sure?"),
primaryButton: .default (Text("Yes")) {
handleGameOver()
},
secondaryButton: .destructive (
Text("No (try again)"))
)
}
As you can see below, handleGameOver() updates two bools in viewModel, which is "observed" by an SKScene where "showingSolution == true" adds a childNode to the scene.
func handleGameOver() {
viewModel.showingSolution = true
viewModel.respondToTap = false
gameOver = true
}
For Further Reference...
Here is how I have things set up:
The GameViewModel:
final class GameViewModel: ObservableObject {
#Published var showingAlert = false
#Published var tapOnTarget = false
#Published var respondToTap = true
#Published var showingSolution = false
}
In the SwiftUI View:
struct GameView: View {
#ObservedObject var viewModel: GameViewModel
#Binding var showingGameScene : Bool
#Binding var gameOver: Bool
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: 400, height: 300)
scene.scaleMode = .aspectFit
scene.backgroundColor = UIColor(.clear)
scene.viewModel = viewModel
return scene
}
var body: some View { ...
SpriteView(scene: scene)
...
Finally, in the SKScene:
class GameScene: SKScene {
var viewModel: GameViewModel?
...
"showingAlert" is set to true with "viewModel?.showingAlert = true" in "touchesBegan."
I can't be way off, since things work on the second attempt. But clearly that's not good enough.
What am I doing wrong?🤔

Chastened by a comment from Cuneyt, I revisited my problematic post and was able to spot my error in the process:
In the GameView, I was using
#ObservedObject var viewModel: GameViewModel
The object is created in the GameView, so I needed to use:
#StateObject var viewModel: GameViewModel
The discussion at
What is the difference between ObservedObject and StateObject in SwiftUI
was helpful.

Related

SwiftUI - Binding in ObservableObject

Let's say we have a parent view like:
struct ParentView: View {
#State var text: String = ""
var body: some View {
ChildView(text: $text)
}
}
Child view like:
struct ChildView: View {
#ObservedObject var childViewModel: ChildViewModel
init(text: Binding<String>) {
self.childViewModel = ChildViewModel(text: text)
}
var body: some View {
...
}
}
And a view model for the child view:
class ChildViewModel: ObservableObject {
#Published var value = false
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
...
}
Making changes on the String binding inside the child's view model makes the ChildView re-draw causing the viewModel to recreate itself and hence reset the #Published parameter to its default value. What is the best way to handle this in your opinion?
Cheers!
The best way is to use a custom struct as a single source of truth, and pass a binding into child views, e.g.
struct ChildViewConfig {
var value = false
var text: String = ""
// mutating funcs for logic
mutating func reset() {
text = ""
}
}
struct ParentView: View {
#State var config = ChildViewConfig()
var body: some View {
ChildView(config: $config)
}
}
struct ChildView: View {
#Binding var config: ChildViewConfig
var body: some View {
TextField("Text", text: $config.text)
...
Button("Reset") {
config.reset()
}
}
}
"ViewConfig can maintain invariants on its properties and be tested independently. And because ViewConfig is a value type, any change to a property of ViewConfig, like its text, is visible as a change to ViewConfig itself." [Data Essentials in SwiftUI WWDC 2020].

How to use #FocusState with view models?

I'm using view models for my SwiftUI app and would like to have the focus state also in the view model as the form is quite complex.
This implementation using #FocusState in the view is working as expected, but not want I want:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
Button("Set Focus") {
hasFocus = true
}
}
}
}
class ViewModel: ObservableObject {
#Published var textField: String = ""
}
How can I put the #FocusState into the view model?
Assuming you have in ViewModel as well
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
...
}
you can use it like
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.onChange(of: hasFocus) {
viewModel.hasFocus = $0 // << write !!
}
.onAppear {
self.hasFocus = viewModel.hasFocus // << read !!
}
}
}
as well as the same from Button if any needed.
I faced the same problem and ended up writing an extension that can be reused to sync both values. This way the focus can also be set from the view model side if needed.
class ViewModel: ObservableObject {
#Published var hasFocus: Bool = false
}
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#FocusState private var hasFocus: Bool
var body: some View {
Form {
TextField("Text", text: $viewModel.textField)
.focused($hasFocus)
}
.sync($viewModel.hasFocus, with: _hasFocus)
}
}
extension View {
func sync<T: Equatable>(_ binding: Binding<T>, with focusState: FocusState<T>) -> some View {
self
.onChange(of: binding.wrappedValue) {
focusState.wrappedValue = $0
}
.onChange(of: focusState.wrappedValue) {
binding.wrappedValue = $0
}
}
}

Changing Published variable automatically navigating to root View, while accessing the variable by environment object. - IOS 14.5

I have a settings Viewmodel. When I am changing a published variable by Toogle it is Automatically going back to a previous view.
I am accessing that published variable in Content View to change the language of the app.
If I do not access the published variable then in the Content view by environment object, changing published variable does not trigger Auto navigation.
Settings View:
struct User4View: View {
#EnvironmentObject var settingsVM: SettingsViewModel
var body: some View {
VStack{
HStack(spacing:12){
Image("language")
.resizable()
.frame(width:20,height: 20)
Text(AppStrings.language)
.mediumHeaderNotBoldTextStyle()
Spacer()
Text("EN")
Toggle("", isOn: $settingsVM.isBangla.didSet { (state) in
print(state)
settingsVM.gotoLoginScreen = false
})
.labelsHidden()
Text("BN")
}
}
Settings ViewModel:
class SettingsViewModel:ObservableObject{
#Published var viewrs:ViewersResponseModel?
#Published var gotoLoginScreen:Bool = false
//static var shared = SettingsViewModel()
#Published var isBangla = UserDefaults.standard.object(forKey: "isBangla") as? Bool ?? false{
didSet{
UserDefaults.standard.set(isBangla, forKey:"isBangla")
print("isBangla",UserDefaults.standard.object(forKey: "isBangla") as? Bool)
}
}
}
ContentView:
struct ContentView: View {
#ObservedObject var settingsVM = SettingsViewModel()
var body: some View {
NavigationView{
Admin2ViewWithoutForm()
}.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(settingsVM)
.environment(\.locale, .init(identifier: settingsVM.isBangla ? "bn-BD" : "en"))
}
}

Issue with viewModel and TextField

I'm not sure whether it's a SwiftUI bug or it's my fault:
When I type some text in a TextField and press the return button on my keyboard (in order to hide my keyboard), the typed text is removed and the TextField is empty again. I've tried this solution on different simulators and on a real device as well. The issue appears every time. I'm using iOS 14.3, Xcode 12.4
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
CreateNewCard(viewModel: CreateNewCardViewModel())
})
}
}
#SwiftPunk: Here is my second question:
Let's say my view model has an additional parameter (id):
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
This parameter needs to be passed when I create the view to my viewModel. For this example let's say we iterate over some elements that have the id:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// is the same problem as before
// because now I create every time a new viewModel, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
Your issue is here, that you did not create a StateObject in main View, and every time you pressed the key on keyboard you created a new model which it was empty as default!
import SwiftUI
struct ContentView: View {
#State var showNew = false
#StateObject var viewModel: CreateNewCardViewModel = CreateNewCardViewModel() // <<: Here
var body: some View {
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
CreateNewCard(viewModel: viewModel)
})
}
}
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
class CreateNewCardViewModel: ObservableObject {
#Published var definition: String = ""
}

SwiftUI ObservedObject not updating

This is the code I have:
import SwiftUI
import Combine
struct ContentView: View {
#State var searchText: String = ""
#State private var editMode = EditMode.inactive
#ObservedObject var contentVM = ContentVM()
var body: some View {
if self.contentVM.loading {
Text("Loading")
} else {
Text("Not loading")
}
}
}
final class ContentVM: ObservableObject {
#Published var loading = true
private var timer: Timer = Timer()
init() {
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in
self.loading = false
print("Not loading any more")
}
}
}
I can see that the console prints out "Not loading any more" correctly, but the content view does not update to show the "Not loading" text.
Edit: It seems objectWillChange.send() does the job, but I feel like a primitive such as a boolean should automatically notify once updated. Am I incorrect?