如何在SwiftUI中子类化@State属性包装器

3

我有一个@State变量,我想给它添加某些约束条件,就像这个简化的例子:

@State private var positiveInt = 0 {
    didSet {
        if positiveInt < 0 {
            positiveInt = 0
        }
    }
}

然而,这看起来不太好(虽然似乎是有效的),但我真正想做的是以某种方式对属性包装器@State进行子类化或扩展,以便可以在其setter中添加此约束条件。但我不知道如何实现。这是否可行?


1
State 是一个结构体,所以你不能对它进行子类化,而且据我所知,你也不能应用多个属性包装器。编写自己的属性包装器可能会起作用,但我不知道你需要与 SwiftUI 的更新循环交互的 API 是什么。很抱歉只能带来这些坏消息。 - gujci
@gujci "与SwiftUI的更新循环交互" - 它可能只是简单地调用body属性吗?还是像在UIKit中调用drawRect:一样,这是一个大大的NO? - turingtested
我没有文档,也没有尝试获取self.body,但我认为这可能是被禁止的。 - gujci
我认为你应该稍微调整一下你的问题?因为解决你的“positiveInt”问题的明显方法是使用“Uint”而不是“Int”。 - user28434'mstep
@turingtested稍微改进了我的答案。 - superpuccio
请查看:@twostraws 的“使用DynamicProperty创建自定义属性包装器” https://www.hackingwithswift.com/plus/intermediate-swiftui/creating-a-custom-property-wrapper-using-dynamicproperty - Heestand XYZ
2个回答

3

由于@State是一个Struct,因此您无法对其进行子类化。您正在尝试操纵模型,因此不应将此逻辑放在视图中。您至少应该按以下方式依赖于您的视图模型:

class ContentViewModel: ObservableObject {
    @Published var positiveInt = 0 {
        didSet {
            if positiveInt < 0 {
                positiveInt = 0
            }
        }
    }
}

struct ContentView: View {
    @ObservedObject var contentViewModel = ContentViewModel()

    var body: some View {
        VStack {
            Text("\(contentViewModel.positiveInt)")
            Button(action: {
                self.contentViewModel.positiveInt = -98
            }, label: {
                Text("TAP ME!")
            })
        }
    }
}

但由于SwiftUI不是一个事件驱动的框架(它完全围绕着数据、模型、绑定等内容),我们应该习惯于不对事件做出反应,而是设计视图以“始终与模型保持一致”。在您的示例和我在此之前的回答中,我们正在对整数更改做出反应,覆盖其值并强制创建视图。更好的解决方案可能是:

class ContentViewModel: ObservableObject {
    @Published var number = 0
}

struct ContentView: View {
    @ObservedObject var contentViewModel = ContentViewModel()

    private var positiveInt: Int {
        contentViewModel.number < 0 ? 0 : contentViewModel.number
    }

    var body: some View {
        VStack {
            Text("\(positiveInt)")
            Button(action: {
                self.contentViewModel.number = -98
            }, label: {
                Text("TAP ME!")
            })
        }
    }
}

甚至更简单(因为基本上已经没有逻辑):
struct ContentView: View {
    @State private var number = 0

    private var positiveInt: Int {
        number < 0 ? 0 : number
    }

    var body: some View {
        VStack {
            Text("\(positiveInt)")
            Button(action: {
                self.number = -98
            }, label: {
                Text("TAP ME!")
            })
        }
    }
}

实际上,我想保留对视图的约束,因为它是一个只能显示一定范围内值的控件。因此,使用计算属性的最后一个建议最适合。 - turingtested

2
您无法同时应用多个propertyWrapper,但是您可以使用两个单独的封装值。首先创建一个将值夹紧到Range的封装值:
@propertyWrapper
struct Clamping<Value: Comparable> {
    var value: Value
    let range: ClosedRange<Value>

    init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
        precondition(range.contains(value))
        self.value = value
        self.range = range
    }

    var wrappedValue: Value {
        get { value }
        set { value = min(max(range.lowerBound, newValue), range.upperBound) }
    }
}

接下来,创建一个 ObservableObject 作为您的后端存储:

class Model: ObservableObject {

    @Published
    var positiveValue: Int = 0

    @Clamping(0...(.max))
    var clampedValue: Int = 0 {
        didSet { positiveValue = clampedValue }
    }
}

现在您可以在内容视图中使用此功能:
    @ObservedObject var model: Model = .init()

    var body: some View {
        Text("\(self.model.positiveValue)")
            .padding()
            .onTapGesture {
                 self.model.clampedValue += 1
            }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接