学习使用SwiftUI开发MacOS应用 - 第3节 文本输入框美化及相关事件处理

SwiftUI 在开发 MacOS 应用的过程中,文本框的美化及事件处理是一个相当复杂的过程,也有很多人在这里卡住无法继续开发项目,也可能是因为 SwiftUI 这个项目的成熟度问题 且iOS 的优先适配等级比MacOS的高,要想在MacOS 中使得文本框可以商用,确实需要折腾很久,可以这么说 SwiftUI 的文本框在MacOS 上只能算是个半成品,网络上的资料文档很多都没能解决这个问题,像是走到一个死胡同里出不来了。
问题1: 圆角输入框
问题2: 调整文本框的高度设置。
问题3:   文本框中的内边距问题
问题4: 高亮选中,及圆角高亮选中区域。
问题5: 输入框获得焦点,及失去焦点监听。
问题6:   置入焦点 (比如登录没有填写信息的时候需要置入焦点)
以上这几个问题,够你翻阅所有收费免费官方第三方的文档,都得不到任何可以决定问题的帮助信息。

我们先在做一下圆角,SwiftUI 没有直接对控件的圆角进行设置的选项,要制作圆角 需要分两步实现,第一步 就是 做一个圆角区域的视图并填充背景,然后将这个视图设置到  .background 上,SwiftUI 的 .background 几乎什么视图都可以装,第二步再做一个圆角区域然后描边并设置到 覆盖层上,也就是  .overlay  可以理解为  背景层,前景层,覆盖层 3个层。
截图贴代码:ContentView.swift

 

struct ContentView: View {

    @State var text1 = ""

    var body: some View {

        GeometryReader { proxy in
            VStack(spacing: 10) {
                Text("Hello, World!")
                        .frame(maxWidth: .infinity, maxHeight: 80)

                TextField("这个是默认的", text: $text1)
                        .font(.system(size: 14)) //设置字体
                        .padding(10)  //设置间距
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框

                Spacer();
            }
        }
    }
}

这样 圆角就出来了,但是我们发现 我们的输入框是一个大输入框套着一个小输入框呢,这个 可能就得去问苹果了。
当然 使用 .textFieldStyle(PlainTextFieldStyle())  是可以把里面的小输入框边线去掉的,但是当选中以后 会有一条蓝色的选择线如图。


是不是很丑啊,怎么交给客户?
为了能去掉这个蓝色边线,就要开始全网检索,其实 这里 NSTextField 有一个参数可以关闭它  focusRingType 属性值为 .none 就可以了,那怎么设置呢?我们的TextField 没法让我去操作 NSTextField 
比较粗暴的办法是 直接扩展  NSTextField 并覆盖 focusRingType 永远都只返回 .none
代码如下:

import SwiftUI

extension NSTextField{
    open override var focusRingType:NSFocusRingType{
        get {
            return .none
        }
        set{}
    }
}

struct ContentView: View {

    @State var text1 = ""

    var body: some View {

        GeometryReader { proxy in
            VStack(spacing: 10) {
                Text("Hello, World!")
                        .frame(maxWidth: .infinity, maxHeight: 80)

                TextField("这个是默认的", text: $text1)
                        .font(.system(size: 14)) //设置字体
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框

                Spacer();
            }
        }
    }
}

还有另外一种办法,就是通过从 TextField 视图去获得 NSTextField 在修改相应的参数,这个很常见,有一个库可以做到,地址是:

https://github.com/siteline/SwiftUI-Introspect.git

安装包的方法,按下图步骤 然后填入这个地址 下一步就可以了。

安装好以后引入包  import Introspect 我们就可以 在任何地方 把我们的NS控件找出来,具体使用可以看 git 的帮助文档。
修改后的代码如下:

import SwiftUI
import Introspect


struct ContentView: View {
    @State var text1 = ""
    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 10) {
                Text("Hello, World!")
                        .frame(maxWidth: .infinity, maxHeight: 80)

                TextField("这个是默认的", text: $text1)
                        .font(.system(size: 14)) //设置字体
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        //查找到NS控件 并 设置 focusRingType
                        .introspectTextField { (field: NSTextField) in
                            field.focusRingType = .none
                        }
                Spacer();
            }
        }
    }
}

当然还有另外一种做法 就是自己 自定义一个 TextField 来替换 TextField ,那样可以实现更多的设置(后面会讲到)。
现在我们在来看看这个界面,虽然 蓝线是去掉了,但是 在点击准备输入的时候,总是会点不到,需要点中心处才可以点到。

也就是 红边框外的是没法点击到的,也不响应点击事件,只有红框里面的才可以点击到,而且 有时候我们并不想把蓝线去掉,而是希望可以把蓝色边线放到 外面的边框,或者更换其他颜色。

也就是说 我们激活的时候 显示的效果是这样的 , 于是有了一种想法 利用 focus 事件来设置 border 的颜色 达到目的,但是 SwiftUI 根本就没有这个事件只有 onEditingChanged,更奇怪的是 .onTapGesture 手势是对上上图中 红色区域外的边框有效,而点击红色区域内的则无效,演示代码如下。

//
//  ContentView.swift
//  myTest4
//
//  Created by wj008 on 2021/4/1.
//
//

import SwiftUI
import Introspect


struct ContentView: View {
    @State var text1 = ""
    @State var text2 = ""
    @State var focus1 = false
    @State var focus2 = false
    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 10) {
                Text("Hello, World!")
                        .frame(maxWidth: .infinity, maxHeight: 80)

                TextField("这个是默认的1", text: $text1,onEditingChanged: { (b: Bool) -> () in
                    focus1 = b
                })
                        .font(.system(size: 14)) //设置字体
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous)
                                .stroke(focus1 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框

                        //查找到NS控件 并 设置 focusRingType
                        .introspectTextField { (field: NSTextField) in
                            field.focusRingType = .none
                        }

                TextField("这个是默认的2", text: $text2,onEditingChanged: { (b: Bool) -> () in
                    focus2 = b
                })
                        .font(.system(size: 14)) //设置字体
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous)
                                .stroke(focus2 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        //手势事件
                        .onTapGesture{
                            print("这里点击外边框才打印信息,光标置入不会打印信息")
                        }
                        //查找到NS控件 并 设置 focusRingType
                        .introspectTextField { (field: NSTextField) in
                            field.focusRingType = .none
                        }

                Spacer();
            }
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

onEditingChanged 的参数是一个 bool 值,如果为真 就表示你已经开始编辑内容了,如果为假是光标离开输入框。所以当我们鼠标置入输入框的时候 并不会发生任何改变,我们需要开始写字了输入框才会变成红色边框。
如果是这样交给客户,那肯定也会被退货。

那么到这一步后,我们就得先把需要的事件找回来,  onFocusChange  

要找回这个事件,就必须自定义控件才能完成,定义控件就需要实现 NSViewRepresentable 协议 

需要至少实现两个函数
 

//第一个函数 是视图创建的时候会调用
func makeNSView(context: Context) -> NSView
//第二个函数是有数据更新(比如@State 被观察的变量有变化)需要刷新内容的时候调用。
func updateNSView(_ view: NSView, context: Context)

非必要实现的还有两个

//用于创建调度器的类
func makeCoordinator() -> Coordinator
//内部类 调度器,主要实现是用来响应事件后回写相关绑定数据使用的
class Coordinator: NSObject, NSTextFieldDelegate

创建 文件 FTextField.swift 
代码如下:
 


import SwiftUI

/**
自定义 NSFTextField 控件,继承 NSTextField
**/
class NSFTextField: NSTextField {
    //定义一个函数变量,用于后面赋值
    var onFocusChange: (Bool) -> Void = { _ in}
    //光标进入的事件 focus
    open override func becomeFirstResponder() -> Bool {
        let bool = super.becomeFirstResponder()
        if bool {
            onFocusChange(true)
        }
        return bool
    }
    //结束编辑的事件 blur
    open override func textDidEndEditing(_ notification: Notification) {
        onFocusChange(false)
        return super.textDidEndEditing(notification)
    }
}
/**
自定义 FTextField 视图
**/
struct FTextField: NSViewRepresentable {

    var placeholder: String
    var text: Binding<String>
    var focusChange: (Bool) -> Void = { _ in}

    //构造函数
    public init(_ placeholder: String,
                text: Binding<String>,
                onFocusChange: @escaping (Bool) -> Void = { _ in}
    ) {
        self.placeholder = placeholder
        self.text = text
        self.focusChange = onFocusChange
    }
    //生成调度器
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    //生成控件
    func makeNSView(context: Context) -> NSFTextField {
        let textField = NSFTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 50));
        textField.autoresizingMask = [.maxXMargin, .maxYMargin]
        textField.placeholderString = placeholder
        //去背景
        textField.drawsBackground = false
        //去边框
        textField.isBordered = false
        //不需要蓝色激活边框
        textField.focusRingType = .none
        //委托调度器
        textField.delegate = context.coordinator
        //设置关联事件
        textField.onFocusChange = focusChange
        return textField
    }

    //更新的时候 把绑定的 text 写入输入框中
    func updateNSView(_ textField: NSFTextField, context: Context) {
        textField.stringValue = text.wrappedValue
    }

    //调度器
    class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: FTextField
        //调度器构造函数
        init(_ view: FTextField) {
            self.parent = view
        }
        //当结束编辑以后 需要把内容回传给 绑定的 text
        func controlTextDidChange(_ obj: Notification) {
            guard let textField = obj.object as? NSFTextField else {
                return
            }
            self.parent.text.wrappedValue = textField.stringValue
        }
    }
}

在 ContentView.swift 中使用控件

struct ContentView: View {
    @State var text1 = ""
    @State var text2 = ""
    @State var focus1 = false
    @State var focus2 = false
    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 10) {
                Text("Hello, World!")
                        .frame(maxWidth: .infinity, maxHeight: 80)

                TextField("这个是默认的", text: $text1,onEditingChanged: { (b: Bool) -> () in
                    focus1 = b
                })
                        .font(.system(size: 16)) //设置字体
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus1 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        //查找到NS控件 并 设置 focusRingType
                        .introspectTextField { (field: NSTextField) in
                            field.focusRingType = .none
                        }

                FTextField("这个是自定义的", text: $text2,onFocusChange: { (b: Bool) -> () in
                    focus2 = b
                })
                        //自定义的控件 已经无法设置字体,这是因为 SwiftUI 在内部对字体的设置做了一些相应操作,没有源码不知道他是怎么完成设置字体的,所以我们只能解析出来自己设置
                        .font(.system(size: 16)) //设置字体
                        .foregroundColor(.green)
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus2 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                
                Spacer();
            }
        }
    }
}

效果:

已经可以支持 鼠标置入 显示红框,鼠标移走 红框变成灰框。
大概已经完成了我们想要的效果,但是 因此也发生了一些不愉快的事情,我们替换了这个TextField 视图,就会导致 SwiftUI 没法在帮我们设置字体,包括颜色 等等。
这个时候 我们就需要把 NSFTextField 暴露出来给视图设置。
我们暴露两个东西,一个是用于在创建时设置 NSFTextField 的,一个是在更新时调整 NSFTextField 的。
我们添加  withSetting 和  withUpdate 函数参数用于修改设置信息。
更改代码如下:
FTextField.swift  文件

import SwiftUI

/**
自定义 NSFTextField 控件,继承 NSTextField
**/
class NSFTextField: NSTextField {
    //定义一个函数变量,用于后面赋值
    var onFocusChange: (Bool) -> Void = { _ in}
    //光标进入的事件 focus
    open override func becomeFirstResponder() -> Bool {
        let bool = super.becomeFirstResponder()
        if bool {
            onFocusChange(true)
        }
        return bool
    }
    //结束编辑的事件 blur
    open override func textDidEndEditing(_ notification: Notification) {
        onFocusChange(false)
        return super.textDidEndEditing(notification)
    }
}
/**
自定义 FTextField 视图
**/
struct FTextField: NSViewRepresentable {

    var placeholder: String
    var text: Binding<String>
    //用作设置的函数类型参数
    var setting: (NSFTextField) -> Void = { _ in}
    //用作更新的函数类型参数
    var update: (NSFTextField) -> Void = { _ in}
    var focusChange: (Bool) -> Void = { _ in}

    //构造函数
    public init(_ placeholder: String,
                text: Binding<String>,
                withSetting: @escaping (NSFTextField) -> Void = { _ in},
                withUpdate: @escaping (NSFTextField) -> Void = { _ in},
                onFocusChange: @escaping (Bool) -> Void = { _ in}
    ) {
        self.placeholder = placeholder
        self.text = text
        self.update = withUpdate
        self.setting = withSetting
        self.focusChange = onFocusChange
    }
    //生成调度器
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    //生成控件
    func makeNSView(context: Context) -> NSFTextField {
        let textField = NSFTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 50));
        textField.autoresizingMask = [.maxXMargin, .maxYMargin]
        textField.placeholderString = placeholder
        //去背景
        textField.drawsBackground = false
        //去边框
        textField.isBordered = false
        //不需要蓝色激活边框
        textField.focusRingType = .none
        //委托调度器
        textField.delegate = context.coordinator
        //设置关联事件
        self.setting(textField)
        textField.onFocusChange = focusChange
        return textField
    }

    //更新的时候 把绑定的 text 写入输入框中
    func updateNSView(_ textField: NSFTextField, context: Context) {
        self.update(textField)
        textField.stringValue = text.wrappedValue
    }

    //调度器
    class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: FTextField
        //调度器构造函数
        init(_ view: FTextField) {
            self.parent = view
        }
        //当结束编辑以后 需要把内容回传给 绑定的 text
        func controlTextDidChange(_ obj: Notification) {
            guard let textField = obj.object as? NSFTextField else {
                return
            }
            self.parent.text.wrappedValue = textField.stringValue
        }
    }
}


这样我们在 视图中就可以使用暴露出来的值来设置变量了。


struct ContentView: View {
    @State var text1 = ""
    @State var text2 = ""
    @State var focus1 = false
    @State var focus2 = false
    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 10) {
                Text("Hello, World!")
                        .frame(maxWidth: .infinity, maxHeight: 80)

                TextField("这个是默认的", text: $text1, onEditingChanged: { (b: Bool) -> () in
                    focus1 = b
                })
                        .font(.system(size: 16)) //设置字体
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus1 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        //查找到NS控件 并 设置 focusRingType
                        .introspectTextField { (field: NSTextField) in
                            field.focusRingType = .none
                        }

                FTextField("这个是自定义的", text: $text2,
                        withSetting: { (field: NSFTextField) in
                            field.font = NSFont.systemFont(ofSize: 30) //在这里设置字体还是没有改变,这个可能是因为SwiftUI 对内部字体进行接管导致这里也无法设置字体,所以这样设置还有问题
                            field.textColor = NSColor.green //这里设置颜色可以设置了
                        }, onFocusChange: { (b: Bool) -> () in
                    focus2 = b
                })
                        //自定义的控件 已经无法设置字体,这是因为 SwiftUI 在内部对字体的设置做了一些相应操作,没有源码不知道他是怎么完成设置字体的,所以我们只能解析出来自己设置
                        //.font(.system(size: 16)) //设置字体
                        //.foregroundColor(.green)
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus2 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框


                Spacer();
            }
        }
    }
}

完成这一步以后,字体颜色已经可以设置,但是字体的大小依旧无效,可能是SwiftUI 在流式布局中对字体设置进行了一些处理,至于怎么处理,没有源码不知道,为了能设置字体,我需要在 自定义的控件中 嵌套一层 NSView
我的想法是 让 NSView 插入 一个 NSFTextField 然后 返回给 视图的是一个 NSView 这样可以保证 NSView 中的内容比较独立,不受流式布局影响,于是修改自定义控件代码如下。

//
// Created by wj008 on 2021/4/1.
//

import SwiftUI

/**
自定义 NSFTextField 控件,继承 NSTextField
**/
class NSFTextField: NSTextField {
    //定义一个函数变量,用于后面赋值
    var onFocusChange: (Bool) -> Void = { _ in}
    //光标进入的事件 focus
    open override func becomeFirstResponder() -> Bool {
        let bool = super.becomeFirstResponder()
        if bool {
            onFocusChange(true)
        }
        return bool
    }
    //结束编辑的事件 blur
    open override func textDidEndEditing(_ notification: Notification) {
        onFocusChange(false)
        return super.textDidEndEditing(notification)
    }
}
/**
自定义 FTextField 视图
**/
struct FTextField: NSViewRepresentable {

    var placeholder: String
    var text: Binding<String>
    //用作设置的函数类型参数
    var setting: (NSFTextField) -> Void = { _ in}
    //用作更新的函数类型参数
    var update: (NSFTextField) -> Void = { _ in}
    var focusChange: (Bool) -> Void = { _ in}

    //构造函数
    public init(_ placeholder: String,
                text: Binding<String>,
                withSetting: @escaping (NSFTextField) -> Void = { _ in},
                withUpdate: @escaping (NSFTextField) -> Void = { _ in},
                onFocusChange: @escaping (Bool) -> Void = { _ in}
    ) {
        self.placeholder = placeholder
        self.text = text
        self.update = withUpdate
        self.setting = withSetting
        self.focusChange = onFocusChange
    }
    //生成调度器
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    //生成控件
    func makeNSView(context: Context) -> NSView {
        //在外层添加一个 view
        let view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 50))

        let textField = NSFTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 50));
        textField.autoresizingMask = [.maxXMargin, .maxYMargin]
        textField.placeholderString = placeholder
        //去背景
        textField.drawsBackground = false
        //去边框
        textField.isBordered = false
        //不需要蓝色激活边框
        textField.focusRingType = .none
        //委托调度器
        textField.delegate = context.coordinator
        //设置关联事件
        self.setting(textField)
        textField.onFocusChange = focusChange
        //讲我们的 textField 插入到 view 中,确保 textField 不要被SwiftUI外部流式布局调整
        view.addSubview(textField)
        return view
    }

    //更新的时候 把绑定的 text 写入输入框中
    func updateNSView(_ view: NSView, context: Context) {
        //这里修从视图 view 中qu
        guard let textField = view.subviews[0] as? NSFTextField else {
            return
        }
        self.update(textField)
        textField.stringValue = text.wrappedValue
    }

    //调度器
    class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: FTextField
        //调度器构造函数
        init(_ view: FTextField) {
            self.parent = view
        }
        //当结束编辑以后 需要把内容回传给 绑定的 text
        func controlTextDidChange(_ obj: Notification) {
            guard let textField = obj.object as? NSFTextField else {
                return
            }
            self.parent.text.wrappedValue = textField.stringValue
        }
    }
}




视图页面 调用 如下:

struct ContentView: View {
    @State var text1 = ""
    @State var text2 = ""
    @State var focus1 = false
    @State var focus2 = false
    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 10) {
                Text("Hello, World!")
                        .frame(maxWidth: .infinity, maxHeight: 80)

                TextField("这个是默认的", text: $text1, onEditingChanged: { (b: Bool) -> () in
                    focus1 = b
                })
                        .font(.system(size: 16)) //设置字体
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus1 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        //查找到NS控件 并 设置 focusRingType
                        .introspectTextField { (field: NSTextField) in
                            field.focusRingType = .none
                        }

                FTextField("这个是自定义的", text: $text2,
                        withSetting: { (field: NSFTextField) in
                            field.font = NSFont.systemFont(ofSize: 16) //在这里设置字体还是没有改变,这个可能是因为SwiftUI 对内部字体进行接管导致这里也无法设置字体,所以这样设置还有问题
                            field.textColor = NSColor.green //这里设置颜色可以设置了
                            field.lineBreakMode = .byTruncatingHead //设置为单行模式
                        }
                        ,
                        withUpdate: { (field: NSFTextField) in
                            //这里需要在更新的时候 同步更新输入框的大小尺寸,间距也靠x y 调整,否则会因为改变窗口大小 错位
                            field.frame = NSRect(x: 10, y: 5, width: proxy.size.width - 40, height: 30)
                        }
                        , onFocusChange: { (b: Bool) -> () in
                    focus2 = b
                })
                        //自定义的控件 已经无法设置字体,这是因为 SwiftUI 在内部对字体的设置做了一些相应操作,没有源码不知道他是怎么完成设置字体的,所以我们只能解析出来自己设置
                        //.font(.system(size: 16)) //设置字体
                        //.foregroundColor(.green)

                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus2 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        .padding(10)  //设置间距

                Spacer();
            }
        }
    }
}

最终:

这里 如果不纠结外面的边框点击不到,那么其实这个时候已经可以给客户交差了。 我们把 输入框 NSFTextField 的背景色加上,看到的是这个样子。

到这里 红色区域才是可以点击 置入光标的区域,白色区域点了没反应的。
为了解决这个问题 就需要最后的杀手锏 需要在重绘另外一个 东西 NSTextFieldCell, 用于编辑内容的区域是 NSTextFieldCell 去绘制的,如果不自定义这个类,那么是没有办法设置 绿色的文字 上下居中的,也就意味着上边点击不到。
最终代码如下:
FTextField.swift  文件

import SwiftUI

class NSFTextFieldCell: NSTextFieldCell {

    var paddingLeft: CGFloat = 0
    var paddingRight: CGFloat = 0
    var focusRadius: CGFloat = 0
    var focusEnable: Bool = true

    //设置偏移
    func textPadding(left: CGFloat, right: CGFloat) {
        paddingLeft = left
        paddingRight = right
    }

    //设置激活蓝色框可以矩形
    func setFocusRingMask(radius: CGFloat, enable: Bool = true) {
        focusRadius = radius
        focusEnable = enable
    }

    //计算集中惠子区域
    func calculateRect(_ rect: NSRect) -> NSRect {
        var newRect: NSRect = super.drawingRect(forBounds: rect)
        let textSize: NSSize = self.cellSize(forBounds: rect)
        let heightDelta: CGFloat = newRect.size.height - textSize.height
        if heightDelta > 0 {
            newRect.size.height = textSize.height
            newRect.origin.y += heightDelta * 0.5
            newRect.size.width -= (paddingLeft + paddingRight)
            newRect.origin.x += paddingLeft
        }
        return newRect
    }

    //覆盖绘图区域
    override func drawingRect(forBounds theRect: NSRect) -> NSRect {
        return calculateRect(theRect)
    }

    /*绘制蓝色激活区域圆角*/
    open override func drawFocusRingMask(withFrame cellFrame: NSRect, in controlView: NSView) {
        if (!focusEnable) {
            return
        }
        if (focusRadius == 0) {
            return super.drawFocusRingMask(withFrame: cellFrame, in: controlView)
        }
        let bounds: NSRect = cellFrame
        let border: NSBezierPath = NSBezierPath(roundedRect: NSInsetRect(bounds, 1, 1), xRadius: focusRadius, yRadius: focusRadius)
        NSColor(red: 0, green: 0, blue: 0, alpha: 0.9).set()
        border.fill()
        return
    }

    /*选择文本时的绘制区域*/
    override func select(withFrame rect: NSRect, in controlView: NSView, editor textObj: NSText, delegate: Any?, start selStart: Int, length selLength: Int) {
        let newRect = calculateRect(rect)
        return super.select(withFrame: newRect, in: controlView, editor: textObj, delegate: delegate, start: selStart, length: selLength)
    }
}


/**
自定义 NSFTextField 控件,继承 NSTextField
**/
class NSFTextField: NSTextField {
    //定义一个函数变量,用于后面赋值
    var onFocusChange: (Bool) -> Void = { _ in}

    //覆盖构建函数
    public override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        let cell = NSFTextFieldCell()
        cell.isSelectable = true
        cell.isEditable = true
        cell.sendsActionOnEndEditing = true
        cell.isBordered = true
        cell.drawsBackground = true
        //替换单元格
        self.cell = cell
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    //光标进入的事件 focus
    open override func becomeFirstResponder() -> Bool {
        let bool = super.becomeFirstResponder()
        if bool {
            onFocusChange(true)
        }
        return bool
    }

    //结束编辑的事件 blur
    open override func textDidEndEditing(_ notification: Notification) {
        onFocusChange(false)
        return super.textDidEndEditing(notification)
    }

    //用于设置文本左右边距
    open func textPadding(left: CGFloat, right: CGFloat) {
        guard  let cell = self.cell as? NSFTextFieldCell else {
            return
        }
        cell.textPadding(left: left, right: right)
    }

    //用于设置 蓝色激活边框 的倒角 和 是否显示
    open func setFocusRingMask(radius: CGFloat, enable: Bool = true) {
        guard  let cell = self.cell as? NSFTextFieldCell else {
            return
        }
        cell.setFocusRingMask(radius: radius, enable: enable)
    }

}
/**
自定义 FTextField 视图
**/
struct FTextField: NSViewRepresentable {

    var placeholder: String
    var text: Binding<String>
    //用作设置的函数类型参数
    var setting: (NSFTextField) -> Void = { _ in}
    //用作更新的函数类型参数
    var update: (NSFTextField) -> Void = { _ in}
    var focusChange: (Bool) -> Void = { _ in}

    //构造函数
    public init(_ placeholder: String,
                text: Binding<String>,
                withSetting: @escaping (NSFTextField) -> Void = { _ in},
                withUpdate: @escaping (NSFTextField) -> Void = { _ in},
                onFocusChange: @escaping (Bool) -> Void = { _ in}
    ) {
        self.placeholder = placeholder
        self.text = text
        self.update = withUpdate
        self.setting = withSetting
        self.focusChange = onFocusChange
    }
    //生成调度器
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    //生成控件
    func makeNSView(context: Context) -> NSView {
        //在外层添加一个 view
        let view = NSView(frame: NSRect(x: 0, y: 0, width: 300, height: 50))

        let textField = NSFTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 50));
        textField.autoresizingMask = [.maxXMargin, .maxYMargin]
        textField.placeholderString = placeholder
        textField.backgroundColor = NSColor.white
        //委托调度器
        textField.delegate = context.coordinator
        //设置关联事件
        self.setting(textField)
        textField.onFocusChange = focusChange
        //讲我们的 textField 插入到 view 中,确保 textField 不要被SwiftUI外部流式布局调整
        view.addSubview(textField)
        return view
    }

    //更新的时候 把绑定的 text 写入输入框中
    func updateNSView(_ view: NSView, context: Context) {
        //这里修从视图 view 中qu
        guard let textField = view.subviews[0] as? NSFTextField else {
            return
        }
        self.update(textField)
        textField.stringValue = text.wrappedValue
    }

    //调度器
    class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: FTextField
        //调度器构造函数
        init(_ view: FTextField) {
            self.parent = view
        }
        //当结束编辑以后 需要把内容回传给 绑定的 text
        func controlTextDidChange(_ obj: Notification) {
            guard let textField = obj.object as? NSFTextField else {
                return
            }
            self.parent.text.wrappedValue = textField.stringValue
        }
    }
}


使用代码如下:

import SwiftUI
import Introspect


struct ContentView: View {
    @State var text1 = ""
    @State var text2 = ""
    @State var text3 = ""
    @State var text4 = ""
    @State var focus1 = false
    @State var focus2 = false
    @State var focus3 = false
    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 10) {
                Text("Hello, World!")
                        .frame(maxWidth: .infinity, maxHeight: 80)

                TextField("这个是默认的,需要开始写字", text: $text1, onEditingChanged: { (b: Bool) -> () in
                    focus1 = b
                })
                        .font(.system(size: 16)) //设置字体
                        .padding(10)  //设置间距
                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus1 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        //查找到NS控件 并 设置 focusRingType
                        .introspectTextField { (field: NSTextField) in
                            field.focusRingType = .none
                        }

                FTextField("这个是自定义的,左右间隙太小", text: $text2,
                        withSetting: { (field: NSFTextField) in
                            field.font = NSFont.systemFont(ofSize: 16) //在这里设置字体还是没有改变,这个可能是因为SwiftUI 对内部字体进行接管导致这里也无法设置字体,所以这样设置还有问题
                            field.textColor = NSColor.green //这里设置颜色可以设置了
                            field.lineBreakMode = .byTruncatingHead //设置为单行模式
                            field.isBordered = false
                            field.drawsBackground = false
                            field.backgroundColor = NSColor(red: 255 / 255, green: 220 / 255, blue: 230 / 255,alpha: 1)
                            field.focusRingType = .none
                        }
                        ,
                        withUpdate: { (field: NSFTextField) in
                            //这里需要在更新的时候 同步更新输入框的大小尺寸,间距也靠x y 调整,否则会因为改变窗口大小 错位
                            field.frame = NSRect(x: 0, y: 0, width: proxy.size.width - 20, height: 50)
                        }
                        , onFocusChange: { (b: Bool) -> () in
                    focus2 = b
                })
                        //自定义的控件 已经无法设置字体,这是因为 SwiftUI 在内部对字体的设置做了一些相应操作,没有源码不知道他是怎么完成设置字体的,所以我们只能解析出来自己设置
                        //.font(.system(size: 16)) //设置字体
                        //.foregroundColor(.green)

                        .textFieldStyle(PlainTextFieldStyle()) //去掉边框
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus2 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        .padding(10)  //设置间距


                FTextField("自定义2", text: $text3,
                        withSetting: { (field: NSFTextField) in
                            let attrString = NSAttributedString(string: "修改帮助文字颜色", attributes: [
                                NSAttributedString.Key.foregroundColor: NSColor(red: 1.0, green: 228.0 / 255, blue: 225.0 / 255, alpha: 0.8),
                                NSAttributedString.Key.font: NSFont.systemFont(ofSize: 14)
                            ])
                            field.placeholderAttributedString = attrString
                            field.tag = 12
                            field.font = NSFont.systemFont(ofSize: 14)
                            field.textColor = NSColor(red: 46.0 / 255, green: 139.0 / 255, blue: 87.0 / 255, alpha: 1)
                            field.isBordered = false
                            field.drawsBackground = false
                            field.lineBreakMode = .byTruncatingHead
                            //设置左右边距
                            field.textPadding(left: 10, right: 10)
                            //不使用 蓝色边框
                            field.setFocusRingMask(radius: 0, enable: false)
                            //field.focusRingType = .none
                        },
                        withUpdate: { (field: NSFTextField) in
                            field.frame = NSRect(x: 0, y: 0, width: proxy.size.width - 20, height: 50)
                        },
                        onFocusChange: { (b) in
                            focus3 = b
                        }
                )
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center) //设置尺寸
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white)) //填充圆角背景
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(focus3 ? Color.red : Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1)) //填充圆角边框
                        .padding(10)  //设置间距


                FTextField("自定义使用系统蓝色激活框", text: $text4,
                        withSetting: { (field: NSFTextField) in
                            field.tag = 11
                            field.font = NSFont.systemFont(ofSize: 14)
                            field.textColor = NSColor.red
                            field.isBordered = false
                            field.drawsBackground = false
                            field.lineBreakMode = .byTruncatingHead
                            field.textPadding(left: 10, right: 10)
                            field.setFocusRingMask(radius: 8)
                        },
                        withUpdate: { (field: NSFTextField) in
                            field.frame = NSRect(x: 0, y: 0, width: proxy.size.width - 20, height: 50)
                        })
                        .frame(width: proxy.size.width - 20, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white))
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255), lineWidth: 1))
                        .padding(10)

                Spacer();
            }
        }
    }
}

最终完成了 圆角的美化 和 事件的操作,后面而后面加入 输入框前置ICON 也可以继续修改出来了。
   

第2张图片中 使用 field.focusRingType = .none 设置,所以 和系统自带的没有形成互斥 ,第3张 中 使用 field.setFocusRingMask(radius: 0, enable: false) 可以和系统默认的形成互斥。


后记:

另加 Button 的美化 和 如何 置入光标,比如点击一个按钮 如果一些值没有填写 就把光标置入没有填写的输入框中。

使用代码如下,自行从代码中解读:


import SwiftUI
import Introspect



struct ContentView: View {
    @State var text1 = ""
    @State var text2 = ""
    @State var text3 = ""
    @State var isFocus3: Bool = false
    var color = Color(red: 220 / 255, green: 220 / 255, blue: 220 / 255)
    var body: some View {
        GeometryReader { proxy in
            VStack(spacing: 10) {

                Text("Hello, World!").padding(.all, 10.0)

                TextField("这个是默认的", text: $text1)
                        .font(.system(size: 14))
                       // .textFieldStyle(PlainTextFieldStyle())
                        .padding(10)
                        .frame(width: proxy.size.width - 20, height: 50, alignment: .center)
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white))
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(color, lineWidth: 1))
                        .introspectTextField { (field: NSTextField) in
                            field.tag = 10  //标识输入框 用于查找
                        }

                FTextField("这个是重载的", text: $text2,
                        withSetting: { (field: NSFTextField) in
                            field.tag = 11  //标识输入框 用于查找
                            field.font = NSFont.systemFont(ofSize: 14)
                            field.textColor = NSColor.red
                            field.isBordered = false
                            field.drawsBackground = false
                            field.lineBreakMode = .byTruncatingHead
                            field.textPadding(left: 10, right: 10)
                            field.setFocusRingMask(radius: 8)
                        },
                        withUpdate: { (field: NSFTextField) in
                            field.frame = NSRect(x: 0, y: 0, width: proxy.size.width - 20, height: 50)
                        })
                        .frame(width: proxy.size.width - 20, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white))
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(color, lineWidth: 1))
                        .padding(10)

                FTextField("测试这个链接", text: $text3,
                        withSetting: { (field: NSFTextField) in
                            let attrString = NSAttributedString(string: "这个是重载的", attributes: [
                                NSAttributedString.Key.foregroundColor: NSColor(red: 1.0, green: 228.0 / 255, blue: 225.0 / 255, alpha: 0.8),
                                NSAttributedString.Key.font: NSFont.systemFont(ofSize: 14)
                            ])
                            field.placeholderAttributedString = attrString
                            field.tag = 12 //标识输入框 用于查找
                            field.font = NSFont.systemFont(ofSize: 14)
                            field.textColor = NSColor(red: 46.0 / 255, green: 139.0 / 255, blue: 87.0 / 255, alpha: 1)
                            field.isBordered = false
                            field.drawsBackground = false
                            field.lineBreakMode = .byTruncatingHead
                            field.textPadding(left: 10, right: 10)
                            field.setFocusRingMask(radius: 0, enable: false)
                            field.focusRingType = .none
                        },
                        withUpdate: { (field: NSFTextField) in
                            field.frame = NSRect(x: 0, y: 0, width: proxy.size.width - 20, height: 50)
                        },
                        onFocusChange: { (focus) in
                            isFocus3 = focus
                        }
                       
                )
                        .frame(width: proxy.size.width - 20, height: 50, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
                        .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.white))
                        .overlay(RoundedRectangle(cornerRadius: 8, style: .continuous).stroke(isFocus3 ? Color.red : color, lineWidth: 1))
                        .padding(10)


                Button(action: {
                    let contentView = NSApp.keyWindow?.contentView
                    if (text1 == "") {
                        print("xxcxc")
                        guard  let field1 = contentView!.viewWithTag(10) as? NSTextField else {
                            print("xxx")
                            return
                        }
                        print("becomeFirstResponder 1")
                        field1.becomeFirstResponder()
                        return
                    }
                    if (text2 == "") {
                        guard  let field2 = contentView!.viewWithTag(11) as? NSFTextField else {
                            return
                        }
                        print("becomeFirstResponder 2")
                        field2.becomeFirstResponder()
                        return
                    }
                    if (text3 == "") {
                        guard  let field3 = contentView!.viewWithTag(12) as? NSFTextField else {
                            return
                        }
                        print("becomeFirstResponder 3")
                        field3.becomeFirstResponder()

                        return
                    }
                }) {
                    Text("点击提交").font(.system(size: 20))
                            .padding()
                            .foregroundColor(Color.white)
                            .background(RoundedRectangle(cornerRadius: 8).fill(Color.blue))
                            .frame(minWidth: 100)
                }
                        .buttonStyle(PlainButtonStyle())
                Spacer();
            }
        }
    }
}


效果图:
  

终于完成本节学习,这一节 也是用的最多的地方,也是在学习过程中比较难弄的地方... 已这个效果 交给客户应该没有问题了。












 



 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值