珍娱客>科技>使用 Swift 属性包装器和 SwiftUI 视图扩展的数据验证解决方案>正文

使用 Swift 属性包装器和 SwiftUI 视图扩展的数据验证解决方案

历史2022-05-150 次阅读

Swift 和 SwiftUI 中的数据验证

使用 Swift 属性包装器和 SwiftUI 视图扩展的数据验证解决方案

无论好坏,Swift 编程语言都没有提供统一的验证方法。 因此,每个开发人员都必须开发自己的方法。 在本文中,我将分享我来之不易的数据验证解决方案。

“犯错是人类,要验证上帝。” ——(不是)亚历山大·波普

在基本层面上,验证需要检测和传达无效数据。 理想情况下,验证逻辑应该是可重用的,并允许在代码库的多个级别合并:在模型、视图和/或两者之间的任何地方。 我提出了一个验证包,它可以实现所有这些目标以及更正和格式化用户输入数据的能力。 这种方法的基础是基于规则的系统。 有了这个系统,我们可以利用 Swift 属性包装器和 SwiftUI 视图扩展的强大功能,在整个代码库中应用我们的验证模式。

创建验证规则

让我们从创建一个相对简单的 Swift 协议开始。 这是代码:

protocol ValidationRule {
    
    associatedtype Value: Equatable
    associatedtype Failure: Error
    
    init()
    
    var fallbackValue: Value { get }
    
    func validate(_ value: Value) -> Result
}

验证逻辑将驻留在 validate 函数中,该函数接受一个值类型并返回一个 Result 类型。 在底层,结果类型是具有两种潜在结果(或案例)的枚举:成功和失败。 重要的是,这两个案例还具有相关的值,允许我们传递有关验证结果的详细信息。

在协议中,有一个定义回退值的属性。 在我们的规则中,这个值可以作为初始值,也可以用来替换无效数据。 后备值对于允许我们创建功能良好的属性包装器(见下文)尤其重要,我们可以在其中为带有扩展的后备值提供默认值。

extension ValidationRule where Value == String {
    var fallbackValue: Value { .init() } // returns empty String
}

extension ValidationRule where Value: ExpressibleByNilLiteral {
    var fallbackValue: Value { .init(nilLiteral: ()) } // returns nil
}

在我们开始创建验证规则之前,我们首先需要定义一个错误类型,我们将使用它来满足我们协议中关联的失败类型。 任何错误类型都可以。 对于此示例,我们将在两行代码中创建一个名为 ErrorMessage 的字符串别名:

typealias ErrorMessage = String
extension ErrorMessage: Error {}

使用我们的新规则协议和错误类型,让我们创建一个简单的规则,然后我们可以使用它来验证字符串。 这是代码:

struct WordRule: ValidationRule {
    
    func validate(_ value: String) -> Result {
        
        // value must be less than or equal to max length
        guard unt <=>

我们可以通过向规则添加额外的初始化程序和变量来定制相同的规则。 在这种情况下,我们将允许修改规则,以便我们可以定义单词的最大长度,如下所示:

struct WordRule: ValidationRule {
    
    let maxLength: Int
    
    init() {
        xLength = 12 // default value for maxLength
    }
    
    init(maxLength: Int) {
        xLength = maxLength
    }
    
    func validate(_ value: String) -> Result {
        
        // value must be less than or equal to max length
        guard unt <=>

我们还可以根据需要添加更多的验证逻辑。 通过在 validate 函数中添加另一个保护语句,我们可以检查以确保所有输入的字符都是字母,如下所示:

        guard lSatisfy({char in Letter}) else {
            return .failure("Word may contain only letters")
        }

到目前为止,我们使用验证结果的 .failure 案例返回了不同的错误类型。 也可以更改成功的返回值。 本质上,我们可以使用这种能力来使我们的规则自动更正或添加格式。 在下面的示例中,我们将修剪单词前后的所有空格。 然后,我们将在验证结果中返回修剪后的值,您可以在下面看到:

    func validate(_ value: String) -> Result {
        
        // transform by trimming whitespace characters
        let value = immingCharacters(in: .whitespaces)
      
        ... validation logic ...
      
        // value has been successfully trimmed and validated
        return .success(value)
    }

现在我们终于创建了一个规则,我们可以将它用作一个独立的验证对象,如下所示:

func testValidation() {
    
    let wordRule = WordRule() // initialize validator object
    
    // create some sample strings to validate
    let tooLong = lidate("toomanycharactersforoneword")
    let hasNumbers = lidate("h3ll0")
    let noErrors = lidate("hello")
    let unTrimmed = lidate(" world")
    
    // print validation results
    print(tooLong) // failure(Word may not exceed 12 characters)
    print(hasNumbers) // failure(Word may contain only letters)
    print(noErrors) // success("hello")
    print(unTrimmed) // success("world")
}

上面的示例相当简单明了,但演示了使用自定义初始化器、包含复杂逻辑和转换值的能力。 结合验证和格式化的用例将包括电话号码、邮政编码等。当我试图预见尽可能多的边缘情况时,好的验证规则经常会在我的手中变得相当重要。

良好的验证是艰苦的工作!

使用 Swift 属性包装器进行验证

当我们探索在整个代码库中使用验证逻辑的不同方法时,验证规则的真正威力变得清晰起来。 使用自定义 Swift 属性包装器执行验证的主要位置是属性级别。 让我们直接进入代码:

@propertyWrapper
struct Validated {
    
    var wrappedValue: lue
    
    private var rule: Rule
    
    // usage: @Validated(Rule()) var value: String = "initial value"
    init(wrappedValue: lue, _ rule: Rule) {
        le = rule
        self.wrappedValue = wrappedValue
    }
}

上面的属性包装器成功地“包装”了我们的验证规则和我们想要验证的值。 不出所料,主初始化器需要验证规则和值。 我们还将为我们的属性包装器包含其他初始化程序,以允许在我们的项目中顺利集成。 在这些初始化器中,我们为我们的规则和我们的值提供默认值,如下所示:

extension Validated {
    
    // usage: @Validated var value: String = "initial value"
    init(wrappedValue: lue) {
        it(wrappedValue: wrappedValue, it())
    }
    
    // usage: @Validated var value {
    init() {
        let rule = it()
        it(wrappedValue: rule.fallbackValue, rule)
    }
}

使用半完整的属性包装器,我们现在可以将属性验证直接合并到我们的模型或 ViewModel 中。 使用我们上面定义的多个属性包装器初始化器,我们有多个语法选项来包装我们的属性。

struct DictionaryEntry {
    // property wrapper using the default init for WordRule
    @Validated(WordRule()) var headWord = ""
}
struct DictionaryEntry {
    // alternate syntax of the default rule initializer
    @Validated var headWord = ""
}
struct DictionaryEntry {
    // property wrapper with custom rule initializer
    @Validated(WordRule(maxLength: 12)) var headWord = ""
}
struct DictionaryEntry {
    // property wrapper where initial property value equals fallback value
    @Validated var headWord
}

但是,我们的属性包装器是不完整的,因为我们无法轻松访问验证结果。 这是我们利用预测值的地方。 在 Swift UI 状态管理中,许多投影值都是 Bindings。 但是在自定义属性包装器中,我们可以将投影值设置为我们喜欢的任何值。 在这种情况下,我们将使用计算属性返回验证结果,如您所见:

extension Validated {
    // provides access to the validation result using $ notation
    public var projectedValue: lidationResult { lidate(wrappedValue) }
}

现在我们已经定义了一个投影值,我们可以随时使用 $ 符号访问我们的验证结果。

func test() {
  let entry = DictionaryEntry(headWord: "h3ll0")
  print(entry.headWord) \\ h3ll0
  print(entry.$headWord) \\ failure(word may contain only letters)
}

尽管我们在上面做了所有的努力,但在某些重要方面,包装后的属性仍然比原始的“未包装”属性更加有限。 具体来说,虽然字符串是可编码和可解码的(又名可编码),但我们的新包装属性不是。 这可以通过扩展我们的属性包装器以包括对这些协议(或任何其他协议)的一致性来解决。 以下是我们如何通过扩展将协议一致性添加到我们的属性包装器中:

extension Validated: Encodable where lue: Encodable {

    func encode(to encoder: Encoder) throws {
        var container = ngleValueContainer()
        switch projectedValue {
        case .success(let validated):
            try container.encode(validated)
        case .failure(_):
            try container.encode(Rule().fallbackValue)
        }
    }
}
extension Validated: Decodable where lue: Decodable {
    
    init(from decoder: Decoder) throws {
        let container = try ngleValueContainer()
        let value = try code(lf)
        it(wrappedValue: value, it())
    }
}

添加 Encodable 一致性不仅仅是一种便利。 请注意,在编码函数中,仅对有效值进行编码。 如果值无效,则不对其进行编码。 相反,使用备用值代替它。 这样可以确保永远不会在编码数据中传递无效值!

SwiftUI 视图中的验证

此时,我们现在可以使用我们的规则对象直接验证数据。 我们还可以使用属性包装器将我们的验证直接合并到模型(或视图模型)中。 但是如果我们想在 SwiftUI 视图中加入验证呢? 视图扩展非常适合此任务,如下所示:

import SwiftUI

extension View {
  
  public func validate(_ value: Binding, rule: Rule, validation: @escaping (lidationResult) -> Void) -> some View where Rule: ValidationRule {
      self
        // when value changes, the escaping function will fire
        .onChange(of: value.wrappedValue) { value in
            let result = lidate(value)
            validation(result) // fire escaping function
        }
        // when field is submitted, the value will be replaced with a valid value
        // this is important if any transformation was made to the value
        // within the validation rule
        .onSubmit {
            let result = lidate(value.wrappedValue)
            if case .success(let validated) = result {
                if value.wrappedValue != validated {
                value.wrappedValue = validated // update value
                }
            }
        }
    }
}

上面发生了一些重要的事情。 请注意,当绑定值更改时会触发转义验证函数。 这通过闭包返回验证结果,然后可以在视图中使用。 通过使用闭包,我们可以随心所欲地处理验证结果,并相应地响应 UI 更改。

当字段被提交时,我们再次检查以确保验证的值在适当的时候与绑定的值匹配。 如果一个值经历了转换(例如修剪空白),它将在提交时更新绑定值。

这是我们的新视图扩展在正确视图中的样子:

import SwiftUI

struct ContentView: View {
    
    @State var word: String = ""
    @State private var errorMessage: String = ""
    
    var body: some View {
        Form {
            Section() {
                HStack {
                    TextField("new word", text: $word)
                    Spacer()
                    Text(errorMessage)
                        .foregroundColor(.red)
                }
            }
        }
        .validate($word, using: WordRule(maxLength: 10)) { result in
            switch result {
            case .success(_): // clear error message
                errorMessage = ""
            case .failure(let message): // display error message
                errorMessage = scription
            }
        }
    }
}

使用我们的新视图扩展,UI 可以立即对无效数据输入做出反应。 可能的 UI 更改包括以文本形式显示错误消息、更改文本颜色或显示错误图标。 在其他情况下,最好阻止保存或暂停导航,直到成功解决错误。 可能性是无止境。

结论

最后,这是我们经过验证的视图在运行中的样子:

使用 Swift 属性包装器和 SwiftUI 视图扩展的数据验证解决方案

我希望您发现本教程很有用,并且它会促使您更多地考虑在项目中使用验证。 如果您喜欢这篇文章,请订阅以鼓励我写更多。

快乐编码!