我正在尝试将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:))
答案 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
}
)
}
}