属性 包装器不影响 TextField

Property Wrapper doesn't affect TextField

我写了 MaxCount propertyWrapper 来限制 StringTextField 中的计数。然而,虽然 Text 视图显示修剪后的 String,但 TextField 显示完整的 String.

我可以通过以下 ViewModifier 实现预期的行为,但这对我来说似乎不是一个好习惯,我想通过 @propertyWrapper.

实现该行为
TextField("Type here...", text: $text)
       .onChange(of: text) { newText in
            // Check if newText has more characters than maxCount, if so trim it.
            guard maxCount < newText.count else { text = newText; return }
            text = String(newText.prefix(maxCount))
        }

MaxCount.swift

@propertyWrapper struct MaxCount<T: RangeReplaceableCollection>: DynamicProperty {
    
    // MARK: Properties
    private var count: Int = 0
    @State private var value: T = .init()
    
    var wrappedValue: T {
        get { value }
        nonmutating set {
            value = limitValue(newValue, count: count)
        }
    }
    
    var projectedValue: Binding<T> {
        Binding(
            get: { value },
            set: { wrappedValue = [=12=] }
        )
    }

    // MARK: Initilizations
    init(wrappedValue: T, _ count: Int) {
        self.count = count
        self._value = State(wrappedValue: limitValue(wrappedValue, count: count))
    }
    
    // MARK: Functions
    private func limitValue(_ value: T, count: Int) -> T {
        guard value.count > count else { return value }
        let lastIndex = value.index(value.startIndex, offsetBy: count - 1)
        let firstIndex = value.startIndex
        return T(value[firstIndex...lastIndex])
    }
    
}

ContentView.swift

struct ContentView: View {

    @MaxCount(5) private var text = "This is a test text"
    
    var body: some View {
        VStack {
            Text(text)
            TextField("Type here...", text: $text)
        }
    }
}

我最终构建了一个新的 TextField,如下所示。

缺点它不支持使用 TextField

中存在的格式化程序进行初始化
struct FilteredTextField<Label: View>: View {
    
    // MARK: Properties
    private let label: Label
    private var bindingText: Binding<String>
    private let prompt: Text?
    private let filter: (String) -> Bool
    @State private var stateText: String
    @State private var lastValidText: String = ""

    // MARK: Initializations
    init(text: Binding<String>, prompt: Text? = nil, label: () -> Label, filter: ((String) -> Bool)? = nil) {
        self.label = label()
        self.bindingText = text
        self.prompt = prompt
        self.filter = filter ?? { _ in true }
        self._stateText = State(initialValue: text.wrappedValue)
    }
    init(_ titleKey: LocalizedStringKey, text: Binding<String>, prompt: Text? = nil, filter: ((String) -> Bool)? = nil) where Label == Text {
        self.label = Text(titleKey)
        self.bindingText = text
        self.prompt = prompt
        self.filter = filter ?? { _ in true }
        self._stateText = State(initialValue: text.wrappedValue)
    }
    init(_ title: String, text: Binding<String>, prompt: Text? = nil, filter: ((String) -> Bool)? = nil) where Label == Text {
        self.label = Text(title)
        self.bindingText = text
        self.prompt = prompt
        self.filter = filter ?? { _ in true }
        self._stateText = State(initialValue: text.wrappedValue)
    }
    
    // MARK: View
    var body: some View {
        TextField(text: $stateText, prompt: prompt, label: { label })
            .onChange(of: stateText) { newValue in
                guard newValue != bindingText.wrappedValue else { return }
                guard filter(newValue) else { stateText = lastValidText; return }
                bindingText.wrappedValue = newValue
            }
            .onChange(of: bindingText.wrappedValue) { newValue in
                if filter(newValue) { lastValidText = newValue }
                stateText = newValue
            }
    }
}

用法

struct ContentView: View {
    @State var test: String = ""
    var body: some View {
        VStack {
            HStack {
                Text("Default TextField")
                TextField(text: $test, label: { Text("Type here...") })
            }
            HStack {
                Text("FilteredTextField")
                FilteredTextField(text: $test, label: { Text("Type here...") }) { inputString in inputString.count <= 5 }
            }
        }
    }
}