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