切换 swiftUI toggle() 时如何触发操作?
Posted
技术标签:
【中文标题】切换 swiftUI toggle() 时如何触发操作?【英文标题】:How can I trigger an action when a swiftUI toggle() is toggled? 【发布时间】:2019-11-21 14:09:55 【问题描述】:在我的 SwiftUI 视图中,我必须在 Toggle() 更改其状态时触发一个动作。切换本身只需要一个绑定。 因此,我尝试在 @State 变量的 didSet 中触发操作。但是 didSet 永远不会被调用。
是否有任何(其他)方式来触发操作?或者有什么方法可以观察到@State 变量的值变化?
我的代码如下所示:
struct PWSDetailView : View
@ObjectBinding var station: PWS
@State var isDisplayed: Bool = false
didSet
if isDisplayed != station.isDisplayed
PWSStore.shared.toggleIsDisplayed(station)
var body: some View
VStack
ZStack(alignment: .leading)
Rectangle()
.frame(width: UIScreen.main.bounds.width, height: 50)
.foregroundColor(Color.lokalZeroBlue)
Text(station.displayName)
.font(.title)
.foregroundColor(Color.white)
.padding(.leading)
MapView(latitude: station.latitude, longitude: station.longitude, span: 0.05)
.frame(height: UIScreen.main.bounds.height / 3)
.padding(.top, -8)
Form
Toggle(isOn: $isDisplayed)
Text("Wetterstation anzeigen")
Spacer()
.colorScheme(.dark)
所需的行为是当 Toggle() 更改其状态时触发操作“PWSStore.shared.toggleIsDisplayed(station)”。
【问题讨论】:
因为我不知道你的应用程序幕后发生的一切,这可能不是一个解决方案,但由于station
是BindableObject
,你不能只替换@987654324 @ 和 Toggle(isOn: $station.isDisplayed)
然后更新 didSet
中的 PWSStore.shared
isDisplayed
PWS
类?
@graycampbell 理论上可行(这是我之前尝试过的)。不幸的是,我的 PWS 类(它是一个核心日期实体)的 didChangeValue(forKey:) 函数经常被调用。在某些情况下(例如按下切换按钮),“isDisplayed”的值确实发生了变化(--> 应该触发该操作)。在其他情况下,'isDisplayed' 的值会用旧值“更新”(--> 操作不必被触发)。我还没有找到区分这两种情况的方法。因此,我尝试直接在视图中触发操作。
【参考方案1】:
首先,你真的知道station.isDisplayed
的额外KVO 通知是个问题吗?您是否遇到性能问题?如果没有,请不要担心。
如果您遇到性能问题并且您已经确定它们是由于过多的station.isDisplayed
KVO 通知引起的,那么接下来要尝试的是消除不需要的 KVO 通知。您可以通过切换到手动 KVO 通知来做到这一点。
将此方法添加到station
的类定义中:
@objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool return false
并使用 Swift 的 willSet
和 didSet
观察者手动通知 KVO 观察者,但前提是值发生变化:
@objc dynamic var isDisplayed = false
willSet
if isDisplayed != newValue willChangeValue(for: \.isDisplayed)
didSet
if isDisplayed != oldValue didChangeValue(for: \.isDisplayed)
【讨论】:
谢谢,罗伯!您的第一行代码已经完成了这项工作。@objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool return false
我不完全理解后台的机制(Apple 文档并没有太大帮助),但似乎这条线只会使一些通知静音。创建 PWS 类的实例或设置 isDisplayed
的值(但未更改)时,不会发送通知。但是当一个 SwiftUI 视图实际上改变了isDisplayed
的值时,仍然有一个通知。对于我的应用,这正是我需要的行为。【参考方案2】:
你可以试试这个(这是一种解决方法):
@State var isChecked: Bool = true
@State var index: Int = 0
Toggle(isOn: self.$isChecked)
Text("This is a Switch")
if (self.isChecked)
Text("\(self.toggleAction(state: "Checked", index: index))")
else
CustomAlertView()
Text("\(self.toggleAction(state: "Unchecked", index: index))")
在它下面,创建一个这样的函数:
func toggleAction(state: String, index: Int) -> String
print("The switch no. \(index) is \(state)")
return ""
【讨论】:
【参考方案3】:我觉得还可以
struct ToggleModel
var isWifiOpen: Bool = true
willSet
print("wifi status will change")
struct ToggleDemo: View
@State var model = ToggleModel()
var body: some View
Toggle(isOn: $model.isWifiOpen)
HStack
Image(systemName: "wifi")
Text("wifi")
.accentColor(.pink)
.padding()
【讨论】:
【参考方案4】:class PWSStore : ObservableObject
...
var station: PWS
@Published var isDisplayed = true
willSet
PWSStore.shared.toggleIsDisplayed(self.station)
struct PWSDetailView : View
@ObservedObject var station = PWSStore.shared
...
var body: some View
...
Toggle(isOn: $isDisplayed) Text("Wetterstation anzeigen")
...
在这里演示https://youtu.be/N8pL7uTjEFM
【讨论】:
【参考方案5】:我找到了一个更简单的解决方案,只需使用 onTapGesture:D
Toggle(isOn: $stateChange)
Text("...")
.onTapGesture
// Any actions here.
【讨论】:
它也会触发,即使在文本被点击时也是如此。我认为这不是一个好的解决方案。【参考方案6】:这是我的方法。我遇到了同样的问题,但决定将 UIKit 的 UISwitch 包装到一个符合 UIViewRepresentable 的新类中。
import SwiftUI
final class UIToggle: UIViewRepresentable
@Binding var isOn: Bool
var changedAction: (Bool) -> Void
init(isOn: Binding<Bool>, changedAction: @escaping (Bool) -> Void)
self._isOn = isOn
self.changedAction = changedAction
func makeUIView(context: Context) -> UISwitch
let uiSwitch = UISwitch()
return uiSwitch
func updateUIView(_ uiView: UISwitch, context: Context)
uiView.isOn = isOn
uiView.addTarget(self, action: #selector(switchHasChanged(_:)), for: .valueChanged)
@objc func switchHasChanged(_ sender: UISwitch)
self.isOn = sender.isOn
changedAction(sender.isOn)
然后它是这样使用的:
struct PWSDetailView : View
@State var isDisplayed: Bool = false
@ObservedObject var station: PWS
...
var body: some View
...
UIToggle(isOn: $isDisplayed) isOn in
//Do something here with the bool if you want
//or use "_ in" instead, e.g.
if isOn != station.isDisplayed
PWSStore.shared.toggleIsDisplayed(station)
...
【讨论】:
对于@Philipp Serflings 方法:附加 TapGestureRecognizer 对我来说不是一个选项,因为当您执行“滑动”来切换切换时它不会触发。而且我不希望失去 UISwitch 的功能。并且使用绑定作为代理就可以了,但我不觉得这是一种 SwiftUI 的方式,但这可能是一个品味问题。我更喜欢视图声明本身中的闭包 非常好。避免任何时间问题,并提供“干净”的视图并维护所有 UISwitch 功能。 谢谢@Tall Dane!但我想现在我会使用 SwiftUI 2 附带的 onChanged 修饰符:)。【参考方案7】:基于@Legolas Wang 的回答。
当您从开关中隐藏原始标签时,您可以仅将 tapGesture 附加到开关本身
HStack
Text("...")
Spacer()
Toggle("", isOn: $stateChange)
.labelsHidden()
.onTapGesture
// Any actions here.
【讨论】:
这里最好的解决方案!仅供参考 - 在 isOn 状态实际更改之前调用 onTap,因此我还必须为 onTap 操作添加 0.1 秒的延迟,以便在调用操作之前,isOn 状态有时间切换。谢谢!【参考方案8】:在我看来,最简洁的方法是使用自定义绑定。 有了它,您就可以完全控制切换开关的实际切换时间
import SwiftUI
struct ToggleDemo: View
@State private var isToggled = false
var body: some View
let binding = Binding(
get: self.isToggled ,
set:
potentialAsyncFunction($0)
)
func potentialAsyncFunction(_ newState: Bool)
//something async
self.isToggled = newState
return Toggle("My state", isOn: binding)
【讨论】:
如果我已经有一个 ZStacks 和 VStacks 会出现很多错误...试图放在里面/外面 - 只有错误 这是解决这个问题的正确方法。没有理由去挖掘 KVO 通知。【参考方案9】:这是一个没有使用 tapGesture 的版本。
@State private var isDisplayed = false
Toggle("", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) (value) in
print("New value is: \(value)")
【讨论】:
这看起来很棒。不需要额外的绑定。 这很好!你也可以.onReceive(Just(isDisplayed)) value in …
我想知道你为什么将 self.isDisplayed 放在方括号中并附加 .publisher.first()。如果是 ObservedObject 而不是 State,您将改写 nameOfObject.$isDisplayed。至少这对我来说似乎有效。
我相信无论出于何种原因更改状态变量时都会触发此代码?【参考方案10】:
以防万一您不想使用额外的功能,弄乱结构 - 使用状态并在任何您想要的地方使用它。我知道这不是事件触发器的 100% 答案,但是,状态将被保存并以最简单的方式使用。
struct PWSDetailView : View
@State private var isToggle1 = false
@State private var isToggle2 = false
var body: some View
ZStack
List
Button(action:
print("\(self.isToggle1)")
print("\(self.isToggle2)")
)
Text("Settings")
.padding(10)
HStack
Toggle(isOn: $isToggle1)
Text("Music")
HStack
Toggle(isOn: $isToggle1)
Text("Music")
【讨论】:
【参考方案11】:适用于 XCode 12
import SwiftUI
struct ToggleView: View
@State var isActive: Bool = false
var body: some View
Toggle(isOn: $isActive) Text(isActive ? "Active" : "InActive")
.padding()
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
【讨论】:
【参考方案12】:这就是我的编码方式:
Toggle("Title", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) (value) in
//Action code here
更新代码(Xcode 12、ios14):
Toggle("Enabled", isOn: $isDisplayed.didSet val in
//Action here
)
【讨论】:
这个超级干净简洁。恕我直言,这应该是正确答案 我无法让你的第二个工作,但经过大量搜索后,第一个版本确实解决了我的问题。第二个版本不会为我编译。谢谢 谢谢! @Manngo 我刚才测试了它。它适用于我的 Xcode12 和 iOS14。你的 Xcode 版本是多少?有没有编译错误信息?我相信第二个更好:) @z33 我使用的是 SCode 12,但目标是 MacOS 10.15 Catalina。我没有直接收到错误消息。编译器需要很长时间才能决定它不能继续。 也同意这应该是答案【参考方案13】:iOS13+
这是一种更通用的方法,您可以将其应用于几乎所有内置的 View
s,例如 Pickers、Textfields、Toggle..
extension Binding
func didSet(execute: @escaping (Value) -> Void) -> Binding
return Binding(
get: self.wrappedValue ,
set:
self.wrappedValue = $0
execute($0)
)
而且用法很简单;
@State var isOn: Bool = false
Toggle("Title", isOn: $isOn.didSet (state) in
print(state)
)
iOS14+
@State private var isOn = false
var body: some View
Toggle("Title", isOn: $isOn)
.onChange(of: isOn) _isOn in
/// use _isOn here..
【讨论】:
这是最干净的实现。对我来说,当视图中的任何其他状态变量发生变化时,就会触发 onReceive。使用此解决方案,该操作仅在附加状态变量更改时运行。【参考方案14】:SwiftUI 2
如果您使用的是 SwiftUI 2 / iOS 14,您可以使用onChange
:
struct ContentView: View
@State private var isDisplayed = false
var body: some View
Toggle("", isOn: $isDisplayed)
.onChange(of: isDisplayed) value in
// action...
print(value)
【讨论】:
这应该是最佳答案。 您好,使用此方法时,如果toggle值保存在数据库中,会调用两次fetch操作。一旦在 init() 处,当我们从视图模型更改 isDisplayed 布尔值时,onChange 再次被激活。有什么办法可以缓解吗?【参考方案15】:这是我编写的一个方便的扩展,用于在按下切换按钮时触发回调。与许多其他解决方案不同,这确实只会在切换切换时触发,而不是在初始化时触发,这对我的用例很重要。这模仿了类似的 SwiftUI 初始化程序,例如用于 onCommit 的 TextField。
用法:
Toggle("My Toggle", isOn: $isOn, onToggled: value in
print(value)
)
扩展:
extension Binding
func didSet(execute: @escaping (Value) -> Void) -> Binding
Binding(
get: self.wrappedValue ,
set:
self.wrappedValue = $0
execute($0)
)
extension Toggle where Label == Text
/// Creates a toggle that generates its label from a localized string key.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// localized key similar to ``Text/init(_:tableName:bundle:comment:)``. See
/// `Text` for more information about localizing strings.
///
/// To initialize a toggle with a string variable, use
/// ``Toggle/init(_:isOn:)-2qurm`` instead.
///
/// - Parameters:
/// - titleKey: The key for the toggle's localized title, that describes
/// the purpose of the toggle.
/// - isOn: A binding to a property that indicates whether the toggle is
/// on or off.
/// - onToggled: A closure that is called whenver the toggle is switched.
/// Will not be called on init.
public init(_ titleKey: LocalizedStringKey, isOn: Binding<Bool>, onToggled: @escaping (Bool) -> Void)
self.init(titleKey, isOn: isOn.didSet(execute: value in onToggled(value) ))
/// Creates a toggle that generates its label from a string.
///
/// This initializer creates a ``Text`` view on your behalf, and treats the
/// title similar to ``Text/init(_:)-9d1g4``. See `Text` for more
/// information about localizing strings.
///
/// To initialize a toggle with a localized string key, use
/// ``Toggle/init(_:isOn:)-8qx3l`` instead.
///
/// - Parameters:
/// - title: A string that describes the purpose of the toggle.
/// - isOn: A binding to a property that indicates whether the toggle is
/// on or off.
/// - onToggled: A closure that is called whenver the toggle is switched.
/// Will not be called on init.
public init<S>(_ title: S, isOn: Binding<Bool>, onToggled: @escaping (Bool) -> Void) where S: StringProtocol
self.init(title, isOn: isOn.didSet(execute: value in onToggled(value) ))
【讨论】:
【参考方案16】:.init是Binding
的构造函数@State var isDisplayed: Bool
Toggle("some text", isOn: .init(
get: isDisplayed ,
set:
isDisplayed = $0
print("changed")
))
【讨论】:
【参考方案17】:这可能会切换它
@Published private(set) var data: [Book] = []
func isBookmarked(article: Book)
guard let index = data.firstIndex(where: $0.id == book.id ) else
return
if(book.bookmarked != nil)
data[index].bookmarked?.toggle()
print("Bookmark added!")
else
data[index].bookmarked = true
print("Bookmark added!")
func deleteBookmark(offset: IndexSet)
data.remove(atOffsets: offset)
【讨论】:
以上是关于切换 swiftUI toggle() 时如何触发操作?的主要内容,如果未能解决你的问题,请参考以下文章
UserDefaults 在 SwiftUI 中与 Toggle 绑定