【问题标题】:SwiftUI call function on variable changeSwiftUI 在变量更改时调用函数
【发布时间】:2020-04-10 05:53:01
【问题描述】:

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

音量控制会根据当前状态volume 改变铃声的进度,当我转动Digital Crown 时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:))


这是当前视图:

【问题讨论】:

  • 你是如何解决的 "@State var songTitle: String = "很长的歌名,应该滚动" 我有同样的问题,我的文字不适合

标签: swift swiftui watchos


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

【讨论】:

  • 我不太明白这个“黑客”。 SwiftUI 的原理是自动响应绑定的变化,那为什么还要添加一些额外的代码呢?您在 '@Published' 属性上设置一个值,然后重绘所有视图。如果你只使用 combine,所有的观察者都会收到通知,然后你在正确的地方做需要做的事情。 $property.sink { // 做这个或那个 }.
【解决方案2】:

正如您所说“过去我会使用 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)%")
  }
 }
}

【讨论】:

  • 真的!它适用于状态变量。在音量控制上,我有一个与 $volume 的绑定,并且据我所见,didSet 在绑定上不起作用。
  • 使用当前对象引用直接访问@State var 而不是**绑定**。传递 self 作为参数
  • didSet 不为我执行。 State 变量被传递到绑定它的单独视图。在这种情况下,显然没有调用父视图中的 didSet。
【解决方案3】:

只是一个猜测,但您可以使用.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
    }
}

【讨论】:

  • 是的,你是对的。这个解决方案是我在最新版本中选择的,因为它是最优雅的。 onChange 相对较新(watchOS 7+),但我看到一些代码将其移植回旧版本
猜你喜欢
  • 2022-11-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-16
  • 1970-01-01
相关资源
最近更新 更多