Swiftui Call Function on Variable Change

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)
}

How can I run an action when a state changes?

You can't use didSet observer on @State but you can on an ObservableObject property.

import SwiftUI
import Combine

final class SelectionStore: ObservableObject {
var selection: SectionType = .top {
didSet {
print("Selection changed to \(selection)")
}
}

// @Published var items = ["Jane Doe", "John Doe", "Bob"]
}

Then use it like this:

import SwiftUI

enum SectionType: String, CaseIterable {
case top = "Top"
case best = "Best"
}

struct ContentView : View {
@ObservedObject var store = SelectionStore()

var body: some View {
List {
Picker("Selection", selection: $store.selection) {
ForEach(FeedType.allCases, id: \.self) { type in
Text(type.rawValue).tag(type)
}
}.pickerStyle(SegmentedPickerStyle())

// ForEach(store.items) { item in
// Text(item)
// }
}
}
}

How to trigger a function via variable change (binding)

You can do the following, create this extension

extension Binding {
func didSet(execute: @escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
let snapshot = self.wrappedValue
self.wrappedValue = $0
execute(snapshot)
}
)
}
}

And the the parent view, not TimerView you can have something like this

//[...]
TimerView(running: $running.didSet{ value in /* your code or function here*/ })
//[...]

Check my answer here

UPDATE 1

Based on the comment what I understood is that the you want to start/stop the timer from outside the TimerView.

I found a couple ways to do that

First one declare the TimerView as a variable in the parent view

struct ContentView: View {
@State var running: Bool = false
@State var timerView: TimerView? = nil
init() {
timerView = TimerView(running: $running)
}
// rest...
}

But depending on your init it can be a pain so I created a small extension that allows you to reference the view like so

extension View {
func `referenced`<T>(by reference: Binding<T>) -> (Self?){
DispatchQueue.main.async { reference.wrappedValue = self as! T }
return self
}
}

The usage of this extension is rather simple

struct ContentView: View {
@State var running: Bool = false
@State var timerView: TimerView? = nil
var body: some View {
TimerView(running: $running).referenced(by: $timerView)
}
}

Then you can control it using for instance a button Button(action: self.timerView.start()) { Text("Start Timer") }

If tested this code against your TimerView but seems there's something wrong in the stop() code. I commented the line cancellable!.cancel() and added a print instead and everything is working as expected.

Here's the test code that I used in Xcode Playground

I hope this is the answer you're looking for.

How to call a function when a @State variable changes

You may use combineLatest in the main view.

               struct MainTimeView: View{
@State private var min : Int = 0
@State private var sec : Int = 0

@State private var myTime: Int = 0
var body: some View {

TimePicker(min: $min, sec: $sec).onReceive(Publishers.CombineLatest(Just(min), Just(sec))) {
self.myTime = $0 * 60 + $1
print(self.myTime)
}
}
}

Execute a method when a variable value changes in Swift

You can simply use a property observer for the variable, labelChange, and call the function that you want to call inside didSet (or willSet if you want to call it before it has been set):

class SharingManager {
var labelChange: String = Model().callElements() {
didSet {
updateLabel()
}
}
static let sharedInstance = SharingManager()
}

This is explained in Property Observers.

I'm not sure why this didn't work when you tried it, but if you are having trouble because the function you are trying to call (updateLabel) is in a different class, you could add a variable in the SharingManager class to store the function to call when didSet has been called, which you would set to updateLabel in this case.



Edited:

So if you want to edit a label from the ViewController, you would want to have that updateLabel() function in the ViewController class to update the label, but store that function in the singleton class so it can know which function to call:

class SharingManager {
static let sharedInstance = SharingManager()
var updateLabel: (() -> Void)?
var labelChange: String = Model().callElements() {
didSet {
updateLabel?()
}
}
}

and then set it in whichever class that you have the function that you want to be called, like (assuming updateLabel is the function that you want to call):

SharingManager.sharedInstance.updateLabel = updateLabel

Of course, you will want to make sure that the view controller that is responsible for that function still exists, so the singleton class can call the function.

If you need to call different functions depending on which view controller is visible, you might want to consider Key-Value Observing to get notifications whenever the value for certain variables change.

Also, you never want to initialize a view controller like that and then immediately set the IBOutlets of the view controller, since IBOutlets don't get initialized until the its view actually get loaded. You need to use an existing view controller object in some way.

Hope this helps.

Is there a way to call a function when a slider is used in swift?

We can use computable in-inline binding with side-effect for this (and you can remove onChange at all), here is a demo

 Slider(value: Binding(get: { noteLength }, 
set: {
// willSet side-effect here // << here !!
noteLenght = $0
// didSet side-effect here // << here !!
}),
in: 0.1...3.01, step: 0.1)
.onChange(of: noteLength, perform: { value in
defaults.set(value, forKey: "noteLengthEx1")
levelIndex = 4
})



Related Topics



Leave a reply



Submit