变量更改时的SwiftUI调用函数

时间:2019-12-17 17:18:05

标签: swift swiftui watchos

我正在尝试将watchOS应用的视图转换为Swift UI。 我想将可以在watchKit中找到的带有自定义控件的音量控件移植到SwiftUI。在下面的图像中,您可以查看视图的当前状态。

音量控制会根据当前状态volume更改铃声的进度,并且在我打开数字表冠时volume也会更改。 没有SwiftUI,就可以在转弯处调用一个函数。这已经改变,系统仅允许我将变量绑定到该变量。

我想知道的是绑定一个变量,并在每次更改该变量时调用一个函数。因为正常的Digital Crown行为无法满足我的需求。

可行但远非完美的一件事是:

.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,但也会在视图的其他所有更新中调用它。

所以我想要的是这样的管道:

皇冠旋转→crownAccumulator更改→函数称为异步→函数更新volume

过去,我会使用didSet来完成此操作,但是不再可用

这里是代码:


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


这是当前视图:
Current View

3 个答案:

答案 0 :(得分:2)

只是猜测,但您可以使用 .onChange 而不是 .onReceive,它只会在值更改时调用。

然而,这里不需要 .onChange.onReceive

这是稍微不同的主题,但此视图中有太多 @State 变量。 (我猜有,因为这是一个相对较旧的问题)

不要忘记 @State 为视图声明了一个新的事实来源, 因此它应该与 parcimony 一起使用(例如用于显示模式、过滤器或任何仅与视图相关的属性)。在这种情况下,就像在许多情况下一样,真相的来源在外面..

因此通过简单地将视图绑定到模型,循环 User action -> value update -> view update 自然完成。

然后,在您的播放器 crownAccumulator 属性的 didSet 中,您对您需要做的模型进行更改,并且视图将根据新的更改自动更新。

请注意,您可能需要在应用中显示其他视图,但仍然可以使用表冠更改音量。所以我建议也将皇冠蓄能器移动到播放器状态。

查看侧面

// The current tune information, provided by model ( Song name, artist, duration..) - Could also be in the PlayerState, since there is only one player and one tune playing in the whole application
@ObservedObject var currentTune: TuneInfo

// The global player state ( Volume, isMuted, isPaused.. )
@EnvironmentObject var playerState: PlayerState

var body: View {
   VStack {
      ...
   }
   .digitalCrownRotation($playerState.crownAccumulator, from: -100.0, through: 100.0, by: 1, sensitivity: .low, isContinuous: true, isHapticFeedbackEnabled: true)
}

模型/控制器端:

struct PlayerState {
    // Published values that will retrigger view rendering when changed
    @Published var isMuted = false
    @Published var volume: Int = 30

    var crownAccumulator: CGFloat {
       didSet { volume = volumeForCrownValue() }
    }

    private func volumeForCrownValue() -> Int {
         // Your formula to compute volume
    }
}

答案 1 :(得分:1)

正如您所说的“ 过去,我会使用didSet来完成此操作,但是不再可用”,我在下面的代码中对其进行了完美的测试

struct pHome: View {
 @State var prog:Int = 2 {
    willSet{
        print("willSet: newValue =\(newValue)  oldValue =\(prog)") 
    }
    didSet{
        print("didSet: oldValue=\(oldValue) newValue=\(prog)")
        //Write code, function do what ever you want todo 
    }
} 
 var body: some View {
VStack{
                Button("Update"){
                    self.prog += 1
                }
                Text("\(self.prog)%")
  }
 }
}

答案 2 :(得分:1)

您可以使用自定义Binding,该自定义调用set中的某些代码。例如:

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: {
                execute($0)
                self.wrappedValue = $0
            }
        )
    }
}