How to Trigger an Action When a Swiftui Toggle() Is Toggled

How can I trigger an action when a swiftUI toggle() is toggled?


iOS 14+

If you're using iOS 14 and higher you can use onChange:

struct ContentView: View {
@State private var isDisplayed = false

var body: some View {
Toggle("", isOn: $isDisplayed)
.onChange(of: isDisplayed) { value in
// action...
print(value)
}
}
}

SwiftUI: Action not triggering with toggle

I am going to shamelessly steal the first part of @Asperi's answer, but the second part is mine...

@State private var isToggle : Bool = false

var body: some View {
Toggle(isOn: self.$isToggle.onUpdate({
print("value did change") // << here !!
})){
Text("Toggle Label ")
}
}

extension Binding {

/// Adds a modifier for this Binding that fires an action when a specific
/// value changes.
///
/// You can use `onUpdate` to trigger a side effect as the result of a
/// `Binding` value changing.
///
/// `onUpdate` is called on the main thread. Avoid performing long-running
/// tasks on the main thread. If you need to perform a long-running task in
/// response to `value` changing, you should dispatch to a background queue.
///
/// The new value is NOT passed into the closure.
///
/// struct PlayerView: View {
/// var episode: Episode
/// @State private var playState: PlayState = .paused
///
/// var body: some View {
/// VStack {
/// Text(episode.title)
/// Text(episode.showTitle)
/// PlayButton(playState: $playState.updated {
/// model.playStateDidChange.update()
/// })
/// }
/// }
/// }
///
/// - Parameters:
/// - action: A closure to run when the value changes.
///
/// - Returns: A new binding value.

func onUpdate(_ action: @escaping () -> Void) -> Binding<Value> {
Binding(get: {
wrappedValue
}, set: { newValue in
wrappedValue = newValue
action()
})
}
}

extension Binding {

/// Adds a modifier for this Binding that fires an action when a specific
/// value changes.
///
/// You can use `updated` to trigger a side effect as the result of a
/// `Binding` value changing.
///
/// `updated` is called on the main thread. Avoid performing long-running
/// tasks on the main thread. If you need to perform a long-running task in
/// response to `value` changing, you should dispatch to a background queue.
///
/// The new value is passed into the closure.
///
/// struct PlayerView: View {
/// var episode: Episode
/// @State private var playState: PlayState = .paused
///
/// var body: some View {
/// VStack {
/// Text(episode.title)
/// Text(episode.showTitle)
/// PlayButton(playState: $playState.updated { newState in
/// model.playStateDidChange(newState)
/// })
/// }
/// }
/// }
///
/// - Parameters:
/// - action: A closure to run when the value changes.
///
/// - Returns: A new binding value.
func updated(_ action: @escaping (_ value: Value) -> Void) -> Binding<Value> {
Binding(get: {
wrappedValue
}, set: { newValue in
wrappedValue = newValue
action(newValue)
})
}
}

There are two Binding extensions to use from iOS 13, watchOS 6 and macOS 10. The first .onUpdate() fires when the binding value changes, but does not give you access to the old or new values. It is JUST for side effects. I used this one above simply because the print() did not need any other value.

If you need to use the newValue in your closure, use .updated. It works very similarly to .onChange(of:) except it modifies the Binding and does not give you access to the old value.

SwiftUI: Call function when toggle is tapped and the State of the variable changes

SwiftUI 2.0

Xcode 12 / iOS 14

As simple as

Toggle(isOn: $isOn) {
Text(isOn ? "On" : "Off")
}.onChange(of: isOn) {
self.sendState(state: $0)
}

SwiftUI 1.0+ (still valid)

A bit more complicated

Toggle(isOn: $isOn) {
Text(isOn ? "On" : "Off")
}.onReceive([isOn].publisher.first()) {
self.sendState(state: $0)
}

Do something when Toggle state changes

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.

SwiftUI: Fire an event when a toggle is switched

In general, you don't want your view to be in charge of executing code when it changes, because your view is not the source of truth - it merely responds to changes in your source of truth.

In this case, what you want is a view model that is in charge of keeping your view's state. When it changes, your view reacts. Then you can have that view model execute code when one of its properties changes (using didSet(), for example).

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

var body: some View {
List {
ForEach(0..<model.sections.count, id: \.self) { index in
Section(header: Text(self.model.sections[index].label as String)) {
Toggle(isOn: self.$model.sections[index].enabled) {
Text("Enabled")
}
}
}
}
.listStyle(GroupedListStyle())
}
}

class ListModel: ObservableObject {
@Published var sections: [ListSection] = [
ListSection(label: "Section One"),
ListSection(label: "Section Two"),
ListSection(label: "Section Three")
]
}

struct ListSection {
var label: String
var enabled: Bool = false {
didSet {
// Here's where any code goes that needs to run when a switch is toggled
print("\(label) is \(enabled ? "enabled" : "disabled")")
}
}
}

How to change state of SwiftUI Toggle externally

You're on the right track by creating a custom Binding with a set function that performs your side effect. But instead of using a State, create a custom Binding that directly modifies the enabled property of your ObservableObject. Example:

import PlaygroundSupport
import SwiftUI

class MyModel: ObservableObject {
@Published var enabled: Bool = false
@Published var sideEffectCount: Int = 0
}

struct RootView: View {
@EnvironmentObject var model: MyModel

var body: some View {
List {
Text("Side effect count: \(model.sideEffectCount)")

Button("Set to false programmatically") {
model.enabled = false
}

Button("Set to true programmatically") {
model.enabled = true
}

Toggle("Toggle without side effect", isOn: $model.enabled)

Toggle("Toggle WITH side effect", isOn: Binding(
get: { model.enabled },
set: { newValue in
withAnimation {
if newValue {
model.sideEffectCount += 1
}
model.enabled = newValue
}
}
))
}
}
}

PlaygroundPage.current.setLiveView(
RootView()
.environmentObject(MyModel())
)

Running code when SwiftUI Toggle Changes Value

The thing that ended up working for me was creating a ViewModel which was also an ObservableObject and then setting the action for the toggle inside of .onTapGesture

ForEach How to close out button effect when another button is toggled

Instead of Bool make it index or row item type, so when button clicked instead of toggle assign current row/item, this will close previous automatically before show current. Of course you need to inject into the view index/item from ForEach level.

1)

    if showRecipeOptions == index {  // index of current row, ...
// if showRecipeOptions == item { // or item of current row
ReditorPopUp(shown: $showRecipeOptions)
.padding(.top, 20)
.padding(.leading, 15)
}


  1.   Button(action:{
    showEditRecipe = index // index of tapped row
    // showEditRecipe = index // or item of tapped row

    }){

Update: simplified demo for idea (tested with Xcode 13.4 / iOS 15.5) - each row has own index (or unique item) and externally provided selection (either via binding or via view model in environment object).

demo

@State private var active: Int? = nil   // external single choice
var body: some View {
List {
ForEach(0..<10) {
Row(active: $active, index: $0) // << inject both
}
}
}

struct Row: View {
@Binding var active: Int?
let index: Int
var body: some View {
HStack {
Text("Item \(index)")
.onTapGesture { active = index } // I'm activated
Spacer()

// if I is activated show something...
if index == active {
Button("Done") { active = nil } // if needed to clean
}
}
.animation(.default, value: active)
}
}

Test module on GitHub

How Can I Pass Function Through A Toggle Swiftui


use

onChange(of:)

for example:

    struct ContentView: View {
@State var toggleIsOn: Bool = false

var body: some View {
Toggle(isOn: $toggleIsOn, label: {Text("Notifications")})
.onChange(of: toggleIsOn) { isOn in
if isOn {
NotificationManager.instance.requestAuthorization()
} else {
NotificationManager.instance.cancelNotifications()
}
}
}
}

Can't use toggle behaviour to trigger .buttonStyle in SWIFTUI

one approach would be to bypass the problem:

            if self.hoursOn {
Button(action: {self.hoursOn.toggle()}) {
Text("Hours").font(.headline)
}.buttonStyle(mainButtonOn())
} else {
Button(action: {self.hoursOn.toggle()}) {
Text("Hours").font(.headline)
}.buttonStyle(mainButtonOff())
}

another more concise approach is this:

struct mainButtonOnOff: ButtonStyle {
let onoff: Bool

init(_ switsh: Bool) {
self.onoff = switsh
}

func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(width: 110, height: 35, alignment: .center)
.background(self.onoff ? Color.blue : Color.white)
.foregroundColor(self.onoff ? Color.white : Color.gray)
.cornerRadius(30)
.overlay(RoundedRectangle(cornerRadius: 30) .stroke(self.onoff ? Color.blue : Color.gray, lineWidth: 1))
}
}

and:

Button(action: {self.hoursOn.toggle()}) {
Text("Hours") .font(.headline)
}.buttonStyle(mainButtonOnOff(self.hoursOn))


Related Topics



Leave a reply



Submit