Toggle a Boolean in a Binding object doesn't update UI - ios

Hello i have an issue with the refresh of my view when i toggle a boolean.
Here is the code
class MyItem: Identifiable {
#Published var isActive: Bool
}
struct myView: View {
#Binding var item: MyItem
var body: some View {
HStack {
Toggle("", isOn: $item.isActive)
Spacer()
TextField("", text: item.isActive ? $text : .constant("")) { isFocused in
guard !isFocused && text.count == 0 else { return }
item.isActive.toggle
}
.background(item.isActive ? Color.white : Color.clear)
}
}
}
when i lose my focus and run item.isActive.toggle it change correctly his value, but my Toggle and my TextField doesn't update their UI (the Toggle is still active and the background of the TextField is still white)
Any idea of what i am missing ?

Binding only updates the view when the wrapped value is updated. However, since your wrapped value is a class, not a struct, the view would only be updated if you replaced the instance with another one, not if you updated a property of the instance.
You need to make MyItem conform to ObservableObject and use #ObservedObject or #StateObject on your view, then it will be updated correctly.
class MyItem: ObservableObject {
#Published var isActive: Bool
}
struct myView: View {
#ObservedObject private var item: MyItem
init(item: MyItem) {
self.item = item
}
var body: some View {
HStack {
Toggle("", isOn: $item.isActive)
Spacer()
TextField("", text: item.isActive ? $text : .constant("")) { isFocused in
guard !isFocused && text.count == 0 else { return }
item.isActive.toggle
}
.background(item.isActive ? Color.white : Color.clear)
}
}
}

Related

Why don't #State parameter changes cause a view update?

I am trying to follow the guidance in a WWDC video to use a #State struct to configure and present a child view. I would expect the struct to be able to present the view, however the config.show boolean value does not get updated when set by the button.
The code below has two buttons, each toggling a different boolean to show an overlay. Toggling showWelcome shows the overlay but toggling config.show does nothing. This seems to be working as intended, I just don't understand why SwiftUI behaves this way. Can someone explain why it's not functioning like I expect, and/or suggest a workaround?
https://developer.apple.com/videos/play/wwdc2020/10040/ # 5:14
struct InformationOverlayConfig {
#State var show = false
#State var title: String?
}
struct InformationOverlay: View {
#Binding var config: InformationOverlayConfig
var body: some View {
if config.title != nil {
Text(config.title!)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
}
}
struct TestView: View {
#State private var configWelcome = InformationOverlayConfig(title: "Title is here")
#State private var showWelcome = false
var body: some View {
VStack {
Text("hello world")
Spacer()
Button("Toggle struct parameter", action: {
configWelcome.show.toggle()
})
Button("Toggle boolean state", action: {
showWelcome.toggle()
})
}
.overlay(
VStack {
InformationOverlay(config: $configWelcome).opacity(configWelcome.show ? 1 : 0)
InformationOverlay(config: $configWelcome).opacity(showWelcome ? 1 : 0)
})
}
You "Config" is not a View. State variables only go in Views.
Also, do not use force unwrapping for config.title. Optional binding or map are the non-redundant solutions.
Lastly, there is no need for config to be a Binding if it actually functions as a constant for a particular view.
struct InformationOverlay: View {
struct Config {
var show = false
var title: String?
}
let config: Config
var body: some View {
VStack {
if let title = config.title {
Text(title)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
// or
config.title.map {
Text($0)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
}
}
}
struct TestView: View {
#State private var configWelcome = InformationOverlay.Config(title: "Title is here")
#State private var showWelcome = false
var body: some View {
VStack {
Text("hello world")
Spacer()
Button("Toggle struct parameter") {
configWelcome.show.toggle()
}
Button("Toggle boolean state") {
showWelcome.toggle()
}
}
.overlay(
VStack {
InformationOverlay(config: configWelcome).opacity(configWelcome.show ? 1 : 0)
InformationOverlay(config: configWelcome).opacity(showWelcome ? 1 : 0)
}
)
}
}

SwiftUI: Reset TextField value from CoreData on change in value of another state variable

I have a ContentView and a NumericView. The selectedIndex is a #State in ContentView. What I want is that if selectedIndex changes, it should reset the answer field in NumericView from CoreData. Below is the code. "av" contains the value of the answer field from CoreData.
struct NumericEntry: View {
#EnvironmentObject var questionAnswerStore: QuestionAnswerStore
#State var answer: String
var questionIndex: Int
init(questionIndex: Int, av: String) {
self.questionIndex = questionIndex
_answer = State(initialValue: av)
}
var body: some View {
VStack {
TextField("Answer", text: $answer)
.textFieldStyle(CustomTextFieldStyle())
.onChange(of: answer) { newValue in
self.questionAnswerStore.getUserAttemptData(selectedIndex: questionIndex).answer = newValue
PersistenceController.shared.saveContext()
}
.keyboardType(.decimalPad)
.padding()
}
}
private struct CustomTextFieldStyle : TextFieldStyle {
public func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding(10)
.background(
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color.secondary.opacity(0.5), lineWidth: 1))
}
}
}
struct ContentView: View {
#State var selectedIndex: Int = 0
#ObservedObject private var questionAnswerStore: QuestionAnswerStore = QuestionAnswerStore.sharedInstance
var body: some View {
NumericEntry(questionIndex: selectedIndex, av: self.questionAnswerStore.getUserAttemptData(selectedIndex: selectedIndex).answer ?? "")
// some code that keeps changing the value of selectedIndex
}
}
I read somewhere that _stateVariable = State(initialValue: "abcd") should set the state of #State stateVariable. In the above code the code
_answer = State(initialValue: av)
executes fine but when it reaches
TextField("Answer", text: $answer)
$answer is still "".
I would prefer a solution where I don't even have to send "av" from the parent component, just selectedIndex should check QuestionAnswerStore for the value of "answer". This could be solved using .onAppear but in my case, the NumericView appears only once and then its parent just keeps changing selectedIndex value, so onAppear doesn't get called again.
Of course, if that is not possible then what's the way out using "av" as above?

Pass binding that is dependent on another binding

I'm rather new to SwiftUI, so please bear with in case I mix up the nomenclature.
I have an Int #State property that is used in a TabView to hold the current index. Then I have a function that takes a Binding<Bool> to display another view.
What I basically want to do is pass a condition into the function whenever my Int state reaches a given value.
Sample code:
import SwiftUI
struct FooTabView: View {
#State private var selected = 0
#State private var shouldShow: Bool = false
var body: some View {
TabView(selection: $selected) {
Text("Hello")
.tabItem {
Text("Foo")
}.tag(0)
Text("World")
.tabItem {
Text("Bar")
}.tag(1)
}.anotherFunctionReturningAView(show: shouldShow)
}
}
Lets say shouldShow should only be true, when selected is equal to 1. How would I do that?
I'm not sure if I understood your goal but you can try:
.anotherFunctionReturningAView(show: .constant(selected == 1))
You can use Proxy Binding, making it depend on your #State variable like that
struct ContentView: View {
#State private var selected = 0
#State private var shouldShow: Bool = false
var body: some View {
TabView(selection: $selected) {
Text("Hello")
.tabItem {
Text("Foo")
}.tag(0)
Text("World")
.tabItem {
Text("Bar")
}.tag(1)
}.alert(isPresented: Binding<Bool>(
get: {
selected == 1 // << Here comes your condition
}, set: {
shouldShow = $0
})
, content: {
Alert(title: Text("Test"))
})
}
}

onReceive not getting called in SwiftUI View when ObservedObject changes

I don't manage to trigger the onReceive method in a SwiftUI View whenever a variable from ObservedObject changes.
I tried two methods: using #Publish and using PassthroughSubject<>
Here is the ViewModel
class MenuViewModel: ObservableObject {
#Published var selectedItems = Set<UUID>()
#Published var currentFocusItem: UUID?
// Output
let newItemOnFocus = PassthroughSubject<(UUID?), Never>()
// This function gets called good :)
func tapOnMenuItem(_ item: MenuItem) {
if selectedItems.contains(item.id) {
//These changes should trigger the onReceive?
currentFocusItem = item.id
newItemOnFocus.send(item.id)
} else {
selectedItems.insert(item.id)
currentFocusItem = nil
newItemOnFocus.send(nil)
}
}
}
Here is the View when trying to catch the changes in #Published var currentFocusItem
struct MenuView: View {
#ObservedObject private var viewModel: MenuViewModel
#State var showPicker = false
#State private var menu: Menu = Menu.mockMenu()
init(viewModel: MenuViewModel = MenuViewModel()) {
self.viewModel = viewModel
}
var body: some View {
VStack {
List(menu.items, selection: $viewModel.selectedItems) { item in
MenuItemView(item: item)
}
Divider()
getBottomView(showPicker: showPicker)
}
.navigationBarTitle("Title")
.navigationBarItems(trailing: Button(action: closeModal) {
Image(systemName: "xmark")
})
.onReceive(viewModel.$currentFocusItem, perform: { itemUUID in
self.showPicker = itemUUID != nil // <-- This only gets called at launch time
})
}
}
The View in the same way but trying to catch the PassthroughSubject<>
.onReceive(viewModel.newItemOnFocus, perform: { itemUUID in
self.showPicker = itemUUID != nil // <-- This never gets called
})
----------EDIT----------
Adding MenuItemView, although viewModel.tapOnMenuItem gets always called, so I am not sure if it's very relevant
MenuItemView is here:
struct MenuItemView: View {
var item: MenuItem
#ObservedObject private var viewModel: MenuViewModel = MenuViewModel()
#State private var isSelected = false
var body: some View {
HStack(spacing: 24) {
Text(isSelected ? " 1 " : item.icon)
.font(.largeTitle)
.foregroundColor(.blue)
.bold()
VStack(alignment: .leading, spacing: 12) {
Text(item.name)
.bold()
Text(item.description)
.font(.callout)
}
Spacer()
Text("\(item.points)\npoints")
.multilineTextAlignment(.center)
}
.padding()
.onTapGesture {
self.isSelected = true
self.viewModel.tapOnMenuItem(self.item). // <-- Here tapOnMenuItem gets called
}
}
func quantityText(isItemSelected: Bool) -> String {
return isItemSelected ? "1" : item.icon
}
}
What am I doing wrong?
Well, here it is - your MenuView and MenuItemView use different instances of view model
1)
struct MenuView: View {
#ObservedObject private var viewModel: MenuViewModel
#State var showPicker = false
#State private var menu: Menu = Menu.mockMenu()
init(viewModel: MenuViewModel = MenuViewModel()) { // 1st one created
2)
struct MenuItemView: View {
var item: MenuItem
#ObservedObject private var viewModel: MenuViewModel = MenuViewModel() // 2nd one
thus, you modify one instance, but subscribe to changes in another one. That's it.
Solution: pass view model via .environmentObject or via argument from MenuView to MenuItemView.

Do something when Toggle state changes

I am using SwiftUI and want to do something when toggle state changes but I cannot find a solution for this.
How can I call a function or do whatever when toggle state changes ?
#State var condition = false
Toggle(isOn: $condition) {
Text("Toggle text here")
}
If you want to do something whenever the toggle state change, you may use didSet.
struct ContentView: View {
#State var condition = false{
didSet{
print("condition changed to \(condition)")
}
}
var body: some View {
let bind = Binding<Bool>(
get:{self.condition},
set:{self.condition = $0}
)
return Toggle(isOn: bind){
Text("Toggle text here")
}
}
}
When we say #State var condition = true, it's type is State<Bool> and not Bool.
when we use binding in toggle ($condition in this case), toggle directly changes the stored value. Meaning it never changes the #State, so didSet will never be triggered.
So here we create and assign to let bind an instance of Binding<Value> struct, and use that Binding struct directly in toggle. Than when we set self.condition = $0, it will trigger didSet.
You can use the .onChange modifier like in the first example below.
However, keep in mind that in most cases we don't need it.
In case of a view setting, you bind to a #State bool value, then you use this bool to modify the view hierarchy, as in the first example.
In case of a change in your model, you bind the toggle to a #Published value in your model or model controller. In this case, the model or model controller receive the change from the switch, do whatever is requested, and the view is rerendered. It must have an #ObservedObject reference to your model.
I show how to use the .onChange modifier in the first example. It simply logs the state.
Remember there should be as few logic as possible in SwiftUI views.
By the way that was already true far before the invention of SwiftUI ;)
Example 1 - #State value. ( Local UI settings )
import SwiftUI
struct ToggleDetailView: View {
#State var isDetailVisible: Bool = true
var body: some View {
VStack {
Toggle("Display Detail", isOn: $isDetailVisible)
.onChange(of: self.isDetailVisible, perform: { value in
print("Value has changed : \(value)")
})
if isDetailVisible {
Text("Detail is visible!")
}
}
.padding(10)
.frame(width: 300, height: 150, alignment: .top)
}
}
struct ToggleDetailView_Previews: PreviewProvider {
static var previews: some View {
ToggleDetailView()
}
}
Example 2 - #ObservedObject value. ( Action on model )
import SwiftUI
class MyModel: ObservableObject {
#Published var isSolid: Bool = false
var string: String {
isSolid ? "This is solid" : "This is fragile"
}
}
struct ToggleModelView: View {
#ObservedObject var model: MyModel
var body: some View {
VStack {
Toggle("Solid Model", isOn: $model.isSolid)
Text(model.string)
}
.padding(10)
.frame(width: 300, height: 150, alignment: .top)
}
}
struct ToggleModelView_Previews: PreviewProvider {
static var previews: some View {
ToggleModelView(model: MyModel())
}
}
Try
struct ContentView: View {
#State var condition = false
var body: some View {
if condition {
callMe()
}
else {
print("off")
}
return Toggle(isOn: $condition) {
Text("Toggle text here")
}
}
func callMe() {
}
}
Karen please check
#State var isChecked: Bool = true
#State var index: Int = 0
Toggle(isOn: self.$isChecked) {
Text("This is a Switch")
if (self.isChecked) {
Text("\(self.toggleAction(state: "Checked", index: index))")
} else {
CustomAlertView()
Text("\(self.toggleAction(state: "Unchecked", index: index))")
}
}
Add function like this
func toggleAction(state: String, index: Int) -> String {
print("The switch no. \(index) is \(state)")
return ""
}
I tend to use:
Toggle(isOn: $condition, label: {
Text("Power: \(condition ? "On" : "Off")")
})
.padding(.horizontal, 10.0)
.frame(width: 225, height: 50, alignment: .center)

Resources