SwiftUI call function on variable change

Sn0wfreeze picture Sn0wfreeze · Dec 17, 2019 · Viewed 10k times · Source

I am trying to convert a view of my watchOS App to Swift UI. I wanted to port the volume control that can be found in watchKit to SwiftUI with custom controls. In the image below you can see the current state of the view.

The volume control changes the progress of the ring according to current state volume and the volume also changes when I turn the Digital Crown. Without SwiftUI it was possible to call a function on the turn of the crown. This has changed and the system only allows me to bind a variable to it.

What I want to do know is to bind a variable and call a function on every change of that variable. Because the normal Digital Crown behavior does not fulfill my needs.

One thing that works, but is far from perfect is:

.digitalCrownRotation($crownAccumulator, from: -100.0, through: 100.0, by: 1, sensitivity: .low, isContinuous: true, isHapticFeedbackEnabled: true)
.onReceive(Just(self.crownAccumulator), perform: self.crownChanged(crownAccumulator:))

OnReceive will be called with every twist of the crown, but it will also be called with every other update to the view.

So what I want is a pipeline like this:

Crown rotates → crownAccumulator changes → Function called async → Function updates volume

In the past I would have done this with a didSet, but this is no longer available

Here the code of it:


    @ObservedObject var group: Group
    @State var animateSongTitle: Bool = false

    @State var songTitle: String = "Very long song title that should be scrolled"
    @State var artist: String = "Artist name"


    @State var volume: Int = 30
    @State var isMuted = false
    @State var crownAccumulator: CGFloat = 0.0


    var body: some View {

       VStack(alignment: .center) {
            TitleView(songTitle: $songTitle, artist: $artist)
            GroupControlButtons(
                skipPreviousAction: skipPrevious,
                skipNextAction: skipNext,
                playPauseAction: playPause,
                group: group)

            ZStack {
                HStack {
                    VolumeControl(
                        volumeLevel: $volume,
                        isMuted: $isMuted,
                        muteAction: self.muteButtonPressed)
                            .frame(minWidth: 40.0, idealWidth: 55, minHeight: 40.0, idealHeight: 55, alignment: .center)
                            .aspectRatio(1.0, contentMode: .fit)


                }
            }


        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
        .edgesIgnoringSafeArea([.bottom])
        .focusable(true)
//        .digitalCrownRotation($crownAccumulator)
        .digitalCrownRotation($crownAccumulator, from: -100.0, through: 100.0, by: 1, sensitivity: .low, isContinuous: true, isHapticFeedbackEnabled: true)
        .onReceive(Just(self.crownAccumulator), perform: self.crownChanged(crownAccumulator:))


Here's the current view:
Current View

Answer

Yonat picture Yonat · Dec 18, 2019

You can use a custom Binding, that calls some code in set. For example from :

extension Binding {
    /// Execute block when value is changed.
    ///
    /// Example:
    ///
    ///     Slider(value: $amount.didSet { print($0) }, in: 0...10)
    func didSet(execute: @escaping (Value) ->Void) -> Binding {
        return Binding(
            get: {
                return self.wrappedValue
            },
            set: {
                self.wrappedValue = $0
                execute($0)
            }
        )
    }
}