【问题标题】:How to subclass the @State property wrapper in SwiftUI如何在 SwiftUI 中子类化 @State 属性包装器
【发布时间】:2019-10-10 11:22:16
【问题描述】:

我有一个@State 变量,我想向它添加一个特定的约束,就像这个简化的例子:

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

然而这看起来不太好(虽然它似乎正在工作)但我真正想做的是子类化或扩展属性包装器@State,以便我可以在它的设置器中添加这个约束。但我不知道该怎么做。甚至可能吗?

【问题讨论】:

  • State 是一个结构,所以你不能继承它,而且,据我所知,你不能应用多个属性包装器。编写你自己的可能有用,但我不知道你需要什么 API 来与 SwiftUI-s 更新循环交互。抱歉,只有坏消息。
  • @gujci "与 SwiftUI-s 更新循环交互" - 调用body 属性会不会这么简单?还是像在 UIKit 中调用 drawRect: 一样,这是一个很大的 NO?
  • 我没有文档,也没有尝试获取self.body,但我认为这有点被禁止
  • 我认为您应该稍微调整一下您的问题?因为您的positiveInt 问题的明显解决方案是使用Uint 而不是Int
  • @turingtested 稍微改进了我的答案。

标签: swift state swiftui


【解决方案1】:

您不能应用多个 propertyWrappers,但可以使用 2 个单独的包装值。从创建一个将值钳位到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
            }
    }

【讨论】:

    【解决方案2】:

    您不能继承 @State,因为 @StateStruct。您正在尝试操作您的模型,因此您不应该将此逻辑放在您的视图中。您至少应该以这种方式依赖您的视图模型:

    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!")
                })
            }
        }
    }
    

    但是由于 SwiftuUI 不是一个事件驱动的框架(它都是关于数据、模型、绑定等的),我们应该习惯于不对事件做出反应,而是将我们的视图设计为“始终与模型保持一致” .在您的示例和我上面的回答中,我们正在对整数变化做出反应,覆盖其值并强制再次创建视图。更好的解决方案可能是这样的:

    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!")
                })
            }
        }
    }
    

    【讨论】:

    • 其实我想保持对视图的约束,因为它是一个只能显示一定范围值的控件。所以最后一个带有计算属性的建议最适合。
    猜你喜欢
    • 2020-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-31
    • 2020-07-13
    • 2020-05-22
    • 2021-01-06
    相关资源
    最近更新 更多