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();
}
}
}
}
效果图:
终于完成本节学习,这一节 也是用的最多的地方,也是在学习过程中比较难弄的地方... 已这个效果 交给客户应该没有问题了。