切换 swiftUI toggle() 时如何触发操作?
How can I trigger an action when a swiftUI toggle() is toggled?
在我的 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.isDisplayed
的额外 KVO 通知是个问题吗?您是否遇到性能问题?如果没有,那就不用担心了。
如果您遇到性能问题并且您已确定它们是由于过多的 station.isDisplayed
KVO 通知引起的,那么接下来要尝试的是消除不需要的 KVO 通知。您可以通过切换到手动 KVO 通知来做到这一点。
将此方法添加到 station
的 class 定义中:
@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) }
}
}
您可以试试这个(这是一种解决方法):
@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 ""
}
我觉得还可以
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()
}
}
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") }
...
}
}
我找到了一个更简单的解决方案,只需使用 onTapGesture:D
Toggle(isOn: $stateChange) {
Text("...")
}
.onTapGesture {
// Any actions here.
}
这是我的方法。我遇到了同样的问题,而是决定将 UIKit 的 UISwitch 包装成一个符合 UIViewRepresentable 的新 class。
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)
}
}
...
}
}
基于@Legolas Wang 的回答。
当您从切换中隐藏原始标签时,您只能将 tapGesture 附加到切换本身
HStack {
Text("...")
Spacer()
Toggle("", isOn: $stateChange)
.labelsHidden()
.onTapGesture {
// Any actions here.
}
}
我认为最简洁的方法是使用自定义绑定。
有了它,您就可以完全控制何时应该实际切换切换
import SwiftUI
struct ToggleDemo: View {
@State private var isToggled = false
var body: some View {
let binding = Binding(
get: { self.isToggled },
set: {
potentialAsyncFunction([=10=])
}
)
func potentialAsyncFunction(_ newState: Bool) {
//something async
self.isToggled = newState
}
return Toggle("My state", isOn: binding)
}
}
这里是没有使用 tapGesture 的版本。
@State private var isDisplayed = false
Toggle("", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) { (value) in
print("New value is: \(value)")
}
以防万一您不想使用额外的功能,请弄乱结构 - 使用状态并在任何您想要的地方使用它。我知道这不是事件触发器的 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")
}
}
}
}
}
}
可用于 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))
}
}
我是这样编码的:
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
})
iOS13+
这是一种更通用的方法,您可以将其应用于几乎所有内置 View
的任何 Binding
,例如选择器、文本字段、切换器..
extension Binding {
func didSet(execute: @escaping (Value) -> Void) -> Binding {
return Binding(
get: { self.wrappedValue },
set: {
self.wrappedValue = [=10=]
execute([=10=])
}
)
}
}
而且用法很简单;
@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..
}
}
斯威夫特用户界面 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)
}
}
}
这是我编写的一个方便的扩展程序,用于在按下切换按钮时触发回调。与许多其他解决方案不同,这实际上只会在切换开关时触发,而不是在 init 上触发,这对我的用例来说很重要。这模仿了类似的 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 = [=11=]
execute([=11=])
}
)
}
}
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) }))
}
}
.init是Binding
的构造函数
@State var isDisplayed: Bool
Toggle("some text", isOn: .init(
get: { isDisplayed },
set: {
isDisplayed = [=10=]
print("changed")
}
))
低于iOS14:
绑定扩展 with Equatable check
public extension Binding {
func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> where Value: Equatable {
Binding(
get: { self.wrappedValue },
set: { newValue in
if self.wrappedValue != newValue { // equal check
self.wrappedValue = newValue
handler(newValue)
}
}
)
}
}
用法:
Toggle(isOn: $pin.onChange(pinChanged(_:))) {
Text("Equatable Value")
}
func pinChanged(_ pin: Bool) {
}
在我的 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.isDisplayed
的额外 KVO 通知是个问题吗?您是否遇到性能问题?如果没有,那就不用担心了。
如果您遇到性能问题并且您已确定它们是由于过多的 station.isDisplayed
KVO 通知引起的,那么接下来要尝试的是消除不需要的 KVO 通知。您可以通过切换到手动 KVO 通知来做到这一点。
将此方法添加到 station
的 class 定义中:
@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) }
}
}
您可以试试这个(这是一种解决方法):
@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 ""
}
我觉得还可以
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()
}
}
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") }
...
}
}
我找到了一个更简单的解决方案,只需使用 onTapGesture:D
Toggle(isOn: $stateChange) {
Text("...")
}
.onTapGesture {
// Any actions here.
}
这是我的方法。我遇到了同样的问题,而是决定将 UIKit 的 UISwitch 包装成一个符合 UIViewRepresentable 的新 class。
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)
}
}
...
}
}
基于@Legolas Wang 的回答。
当您从切换中隐藏原始标签时,您只能将 tapGesture 附加到切换本身
HStack {
Text("...")
Spacer()
Toggle("", isOn: $stateChange)
.labelsHidden()
.onTapGesture {
// Any actions here.
}
}
我认为最简洁的方法是使用自定义绑定。 有了它,您就可以完全控制何时应该实际切换切换
import SwiftUI
struct ToggleDemo: View {
@State private var isToggled = false
var body: some View {
let binding = Binding(
get: { self.isToggled },
set: {
potentialAsyncFunction([=10=])
}
)
func potentialAsyncFunction(_ newState: Bool) {
//something async
self.isToggled = newState
}
return Toggle("My state", isOn: binding)
}
}
这里是没有使用 tapGesture 的版本。
@State private var isDisplayed = false
Toggle("", isOn: $isDisplayed)
.onReceive([self.isDisplayed].publisher.first()) { (value) in
print("New value is: \(value)")
}
以防万一您不想使用额外的功能,请弄乱结构 - 使用状态并在任何您想要的地方使用它。我知道这不是事件触发器的 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")
}
}
}
}
}
}
可用于 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))
}
}
我是这样编码的:
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
})
iOS13+
这是一种更通用的方法,您可以将其应用于几乎所有内置 View
的任何 Binding
,例如选择器、文本字段、切换器..
extension Binding {
func didSet(execute: @escaping (Value) -> Void) -> Binding {
return Binding(
get: { self.wrappedValue },
set: {
self.wrappedValue = [=10=]
execute([=10=])
}
)
}
}
而且用法很简单;
@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..
}
}
斯威夫特用户界面 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)
}
}
}
这是我编写的一个方便的扩展程序,用于在按下切换按钮时触发回调。与许多其他解决方案不同,这实际上只会在切换开关时触发,而不是在 init 上触发,这对我的用例来说很重要。这模仿了类似的 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 = [=11=]
execute([=11=])
}
)
}
}
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) }))
}
}
.init是Binding
的构造函数@State var isDisplayed: Bool
Toggle("some text", isOn: .init(
get: { isDisplayed },
set: {
isDisplayed = [=10=]
print("changed")
}
))
低于iOS14:
绑定扩展 with Equatable check
public extension Binding {
func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> where Value: Equatable {
Binding(
get: { self.wrappedValue },
set: { newValue in
if self.wrappedValue != newValue { // equal check
self.wrappedValue = newValue
handler(newValue)
}
}
)
}
}
用法:
Toggle(isOn: $pin.onChange(pinChanged(_:))) {
Text("Equatable Value")
}
func pinChanged(_ pin: Bool) {
}