我一直在尝试在SwiftUI中创建多行TextField,但是我不知道怎么做。
这是我当前拥有的代码:
struct EditorTextView : View {
@Binding var text: String
var body: some View {
TextField($text)
.lineLimit(4)
.multilineTextAlignment(.leading)
.frame(minWidth: 100, maxWidth: 200, minHeight: 100, maxHeight: .infinity, alignment: .topLeading)
}
}
#if DEBUG
let sampleText = """
Very long line 1
Very long line 2
Very long line 3
Very long line 4
"""
struct EditorTextView_Previews : PreviewProvider {
static var previews: some View {
EditorTextView(text: .constant(sampleText))
.previewLayout(.fixed(width: 200, height: 200))
}
}
#endif
但这是输出:
答案 0 :(得分:25)
它称为TextEditor
struct ContentView: View {
@State var text: String = "Multiline \ntext \nis called \nTextEditor"
var body: some View {
TextEditor(text: $text)
}
}
如果您希望它随着键入而增长,请在其上嵌入如下标签:
ZStack {
TextEditor(text: $text)
Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}
您可以在带有此结构的SwiftUI代码中使用本机UITextView:
struct TextView: UIViewRepresentable {
typealias UIViewType = UITextView
var configuration = { (view: UIViewType) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIViewType {
UIViewType()
}
func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<Self>) {
configuration(uiView)
}
}
struct ContentView: View {
var body: some View {
TextView() {
$0.textColor = .red
// Any other setup you like
}
}
}
优势:
UIKit
中经过多年测试UITextView
答案 1 :(得分:14)
这将UITextView包装为Xcode 11.0 beta 6(仍在Xcode 11 GM种子2上运行):
RUN wget https://download.libsodium.org/libsodium/releases/libsodium-1.0.15.tar.gz \
&& tar xzf libsodium-1.0.15.tar.gz \
&& cd libsodium-1.0.15 \
&& ./configure \
&& make install
答案 2 :(得分:11)
@Meo Flute的答案很好!但这不适用于多阶段文本输入。 并结合@Asperi的答案,这里已解决了该问题,我还添加了对占位符的支持,只是为了好玩!
struct TextView: UIViewRepresentable {
var placeholder: String
@Binding var text: String
var minHeight: CGFloat
@Binding var calculatedHeight: CGFloat
init(placeholder: String, text: Binding<String>, minHeight: CGFloat, calculatedHeight: Binding<CGFloat>) {
self.placeholder = placeholder
self._text = text
self.minHeight = minHeight
self._calculatedHeight = calculatedHeight
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
// Decrease priority of content resistance, so content would not push external layout set in SwiftUI
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.isScrollEnabled = false
textView.isEditable = true
textView.isUserInteractionEnabled = true
textView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)
// Set the placeholder
textView.text = placeholder
textView.textColor = UIColor.lightGray
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = self.text
recalculateHeight(view: textView)
}
func recalculateHeight(view: UIView) {
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if minHeight < newSize.height && $calculatedHeight.wrappedValue != newSize.height {
DispatchQueue.main.async {
self.$calculatedHeight.wrappedValue = newSize.height // !! must be called asynchronously
}
} else if minHeight >= newSize.height && $calculatedHeight.wrappedValue != minHeight {
DispatchQueue.main.async {
self.$calculatedHeight.wrappedValue = self.minHeight // !! must be called asynchronously
}
}
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textViewDidChange(_ textView: UITextView) {
// This is needed for multistage text input (eg. Chinese, Japanese)
if textView.markedTextRange == nil {
parent.text = textView.text ?? String()
parent.recalculateHeight(view: textView)
}
}
func textViewDidBeginEditing(_ textView: UITextView) {
if textView.textColor == UIColor.lightGray {
textView.text = nil
textView.textColor = UIColor.black
}
}
func textViewDidEndEditing(_ textView: UITextView) {
if textView.text.isEmpty {
textView.text = parent.placeholder
textView.textColor = UIColor.lightGray
}
}
}
}
像这样使用它:
struct ContentView: View {
@State var text: String = ""
@State var textHeight: CGFloat = 150
var body: some View {
ScrollView {
TextView(placeholder: "", text: self.$text, minHeight: self.textHeight, calculatedHeight: self.$textHeight)
.frame(minHeight: self.textHeight, maxHeight: self.textHeight)
}
}
}
答案 3 :(得分:10)
好吧,我从@sas方法开始,但实际上需要它看起来和感觉都像具有内容合适的多行文本字段,等等。这就是我所拥有的。希望对其他人有所帮助...使用过的Xcode 11.1。
提供的自定义MultilineTextField具有:
1。内容适合
2。自动对焦
3。占位符
4。提交时
import SwiftUI
import UIKit
fileprivate struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
@Binding var text: String
@Binding var calculatedHeight: CGFloat
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let textField = UITextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = UIFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.isScrollEnabled = false
if nil != onDone {
textField.returnKeyType = .done
}
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
}
fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if result.wrappedValue != newSize.height {
DispatchQueue.main.async {
result.wrappedValue = newSize.height // !! must be called asynchronously
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var calculatedHeight: Binding<CGFloat>
var onDone: (() -> Void)?
init(text: Binding<String>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
self.text = text
self.calculatedHeight = height
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone = self.onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
}
}
struct MultilineTextField: View {
private var placeholder: String
private var onCommit: (() -> Void)?
@Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text } ) {
self.text = $0
self.showingPlaceholder = $0.isEmpty
}
}
@State private var dynamicHeight: CGFloat = 100
@State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
var body: some View {
UITextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit)
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
.overlay(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
}
#if DEBUG
struct MultilineTextField_Previews: PreviewProvider {
static var test:String = ""//some very very very long description string to be initially wider than screen"
static var testBinding = Binding<String>(get: { test }, set: {
// print("New value: \($0)")
test = $0 } )
static var previews: some View {
VStack(alignment: .leading) {
Text("Description:")
MultilineTextField("Enter some text here", text: testBinding, onCommit: {
print("Final text: \(test)")
})
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
Text("Something static here...")
Spacer()
}
.padding()
}
}
#endif
答案 4 :(得分:7)
使用Text()
,您可以使用.lineLimit(nil)
来实现此目的,并且文档建议该应该也适用于TextField()
。但是,我可以确认这目前无法正常工作。
我怀疑有错误-建议您向反馈助手提交报告。我已经完成了,ID是FB6124711。
答案 5 :(得分:6)
当前,最好的解决方案是使用我创建的名为TextView的软件包。
您可以使用Swift软件包管理器进行安装(自述文件中对此进行了说明)。它允许可切换的编辑状态,以及许多自定义项(自述文件中也有详细介绍)。
这是一个例子:
import SwiftUI
import TextView
struct ContentView: View {
@State var input = ""
@State var isEditing = false
var body: some View {
VStack {
Button(action: {
self.isEditing.toggle()
}) {
Text("\(isEditing ? "Stop" : "Start") editing")
}
TextView(text: $input, isEditing: $isEditing)
}
}
}
在该示例中,首先定义两个@State
变量。一种是用于文本,每次键入时都会将其写入文本,另一种是用于TextView的isEditing
状态。
在选择TextView时,将切换isEditing
状态。单击按钮时,也会切换isEditing
状态,该状态将显示键盘,并在true
时选择TextView,在false
时取消选择TextView。
答案 6 :(得分:5)
现在,您可以包装UITextView来创建可组合的View
:
import SwiftUI
import Combine
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
var text = "" {
didSet {
didChange.send(self)
}
}
init(text: String) {
self.text = text
}
}
struct MultilineTextView: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
struct ContentView : View {
@State private var selection = 0
@EnvironmentObject var userData: UserData
var body: some View {
TabbedView(selection: $selection){
MultilineTextView(text: $userData.text)
.tabItemLabel(Image("first"))
.tag(0)
Text("Second View")
.font(.title)
.tabItemLabel(Image("second"))
.tag(1)
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(UserData(
text: """
Some longer text here
that spans a few lines
and runs on.
"""
))
}
}
#endif
答案 7 :(得分:4)
SwiftUI 具有 TextEditor
,它类似于 TextField
,但提供长格式文本输入,可以包装成多行:
var body: some View {
NavigationView{
Form{
Section{
List{
Text(question6)
TextEditor(text: $responseQuestion6).lineLimit(4)
Text(question7)
TextEditor(text: $responseQuestion7).lineLimit(4)
}
}
}
}
}
答案 8 :(得分:3)
SwiftUI TextView(UIViewRepresentable)具有以下可用参数: fontStyle,isEditable,backgroundColor,borderColor和border宽度
TextView(文本:self。$ viewModel.text,fontStyle:.body,isEditable:true,backgroundColor:UIColor.white,borderColor:UIColor.lightGray,borderWidth:1.0) .padding()
TextView(UIViewRepresentable)
struct TextView: UIViewRepresentable {
@Binding var text: String
var fontStyle: UIFont.TextStyle
var isEditable: Bool
var backgroundColor: UIColor
var borderColor: UIColor
var borderWidth: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let myTextView = UITextView()
myTextView.delegate = context.coordinator
myTextView.font = UIFont.preferredFont(forTextStyle: fontStyle)
myTextView.isScrollEnabled = true
myTextView.isEditable = isEditable
myTextView.isUserInteractionEnabled = true
myTextView.backgroundColor = backgroundColor
myTextView.layer.borderColor = borderColor.cgColor
myTextView.layer.borderWidth = borderWidth
myTextView.layer.cornerRadius = 8
return myTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: TextView
init(_ uiTextView: TextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
}
}
答案 9 :(得分:3)
MacOS实施
function BankForm({ className, setImage, ...props }) {
const classes = useStyles();
//States
const [apiType, setApiType] = useState('');
const [openMessage, setOpenMessage] = useState(false);
const [messageType, setMessageType] = useState('');
const [message, setMessage] = useState('');
const [currentBankData, setCurrentBankData] = useState([]);
const [bankId, setBankId] = useState(props.id);
const [imageData, setImageData] = useState('');
const [imageBinairyData, setImageBinairyData] = useState('');
const [test, setTest] = useState('');
let url = `bank/findBank/${bankId}`;
useEffect(() => {
const fetchData = async () => {
const data = await Api(url, 'Get')
setCurrentBankData(data);
}
fetchData();
//data:image/png;base64," + data
}, []);
console.log(currentBankData.image.json) //displays undefined
function submitForm(values) {
// routine to send the request to the server
const url = 'http://localhost:8080/api/bank/updateBank';
const formData = new FormData();
var postData = values;
formData.append(
'bank',
new Blob([JSON.stringify(postData)], {
type: 'application/json',
})
);
if (imageData) formData.append('file', imageData);
//return axios.post(url, formData, config);
if(bankId){
return Api(`bank/updateBank/${bankId}`,"Put",formData)
// Api(`bank/${bankId}`,"Delete")
}else{
return Api(`bank/updateBank`,"Post",formData)
}
}
return (
<div>
<Showmessage
openMessage={openMessage}
closeMessage={() => setOpenMessage(false)}
vertical={'bottom'}
horizontal={'center'}
type={messageType}
message={message}
/>
{/*Formik to validate and send http req */}
<Formik
enableReinitialize={true}
initialValues={{
name: currentBankData.name || '',
address: currentBankData.address || '',
country: currentBankData.country || '',
region: currentBankData.region || '',
city: currentBankData.city || '',
swiftCode: currentBankData.swiftCode || '',
routeCode: currentBankData.routeCode || '',
//image: currentBankData.image || '',
}}
validationSchema={Yup.object().shape({
name: Yup.string().required('Name is required.'),
address: Yup.string().required('Address is required.'),
country: Yup.string().required('Country is required.'),
region: Yup.string().required('Region is required.'),
city: Yup.string().required('City is required.'),
swiftCode: Yup.string().when('region', (region, schema) => {
return ['Europe', 'other'].includes(region)
? schema
.required('SwiftCode is required.')
.matches(
/[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?/i,
'This is not the correct Swift Code'
)
: schema.matches(
/[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?/i,
'This is not the correct Swift Code'
);
}),
routeCode: Yup.string().when('region', {
is: 'USA',
then: Yup.string().required('RouteCode is required.'),
}),
verified: Yup.bool(),
})}
onSubmit={async (
values,
{ resetForm, setErrors, setStatus, setSubmitting }
) => {
try {
//Update Bank
/**/ submitForm(values).then(
setMessageType('success'),
setApiType('Post'),
setOpenMessage(true),
setMessage('Changes Saved'),
props.refreshtable,
);
setOpenMessage(true);
} catch (error) {
setStatus({ success: false });
setErrors({ submit: error.message });
setSubmitting(false);
}
}}
>
{({
errors,
handleBlur,
handleChange,
handleSubmit,
isSubmitting,
setFieldTouched,
setFieldValue,
touched,
values,
}) => (
<form onSubmit={handleSubmit} {...props}>
<Card>
<CardContent>
<Box mt={2}>
<Grid container spacing={3}>
<Grid item xs={6}>
<Avatar
style={{ height: 42, width: 42 }}
src={`data:image/jpeg;base64,${currentBankData.image.data}`}
></Avatar>
<TextField
error={Boolean(touched.name && errors.name)}
fullWidth
helperText={touched.name && errors.name}
label="Bank name"
name="name"
onBlur={handleBlur}
onChange={handleChange}
value={values.name}
variant="outlined"
/>
</Grid>
<Grid item xs={6}>
<LogoUpload
setImageData={setImageData}
bankId={bankId}
imageBinairyData={imageBinairyData}
data={currentBankData}
//myImage ={values.image.data}
//api={`/bank/findBank/${bankId}`}
/>
</Grid>
</Grid>
<Box mt={2}>
<TextField
error={Boolean(touched.address && errors.address)}
fullWidth
helperText={touched.address && errors.address}
label="Address"
name="address"
onBlur={handleBlur}
multiline
rows={5}
onChange={handleChange}
value={values.address}
variant="outlined"
/>
</Box>
<Box mt={2}>
{/*Country Selector */}
<CountryComponent/>
</Box>
<Box mt={2}>
<TextField
fullWidth
label="Region"
name="region"
onBlur={handleBlur}
onChange={handleChange}
InputProps={{
readOnly: true,
}}
value={values.region}
variant="outlined"
/>
</Box>
<Box mt={2}>
<TextField
error={Boolean(touched.city && errors.city)}
fullWidth
helperText={touched.city && errors.city}
label="City"
name="city"
onBlur={handleBlur}
onChange={handleChange}
value={values.city}
variant="outlined"
/>
</Box>
<Box mt={2}>
<TextField
error={Boolean(touched.swiftCode && errors.swiftCode)}
fullWidth
helperText={touched.swiftCode && errors.swiftCode}
label="SwiftCode"
name="swiftCode"
onBlur={handleBlur}
onChange={handleChange}
value={values.swiftCode}
variant="outlined"
/>
</Box>
<Box mt={2}>
<TextField
error={Boolean(touched.routeCode && errors.routeCode)}
fullWidth
helperText={touched.routeCode && errors.routeCode}
label="RouteCode"
name="routeCode"
onBlur={handleBlur}
onChange={handleChange}
value={values.routeCode}
variant="outlined"
/>
</Box>
<Box mt={3}>
<Button
// onClick={props.closeBankDrawer}
variant="contained"
color="secondary"
type="submit"
disabled={isSubmitting}
>
{props.id ? 'Save Changes' : 'Add Bank'}
</Button>
<Button
onClick={props.closeBankDrawer}
variant="contained"
color="secondary"
disabled={isSubmitting}
>
Cancel
</Button>
</Box>
</Box>
</CardContent>
</Card>
</form>
)}
</Formik>
</div>
);
}
export default BankForm;
以及使用方法
struct MultilineTextField: NSViewRepresentable {
typealias NSViewType = NSTextView
private let textView = NSTextView()
@Binding var text: String
func makeNSView(context: Context) -> NSTextView {
textView.delegate = context.coordinator
return textView
}
func updateNSView(_ nsView: NSTextView, context: Context) {
nsView.string = text
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, NSTextViewDelegate {
let parent: MultilineTextField
init(_ textView: MultilineTextField) {
parent = textView
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
self.parent.text = textView.string
}
}
}
答案 10 :(得分:2)
Xcode 12 和 iOS14 可用,真的很简单。
import SwiftUI
struct ContentView: View {
@State private var text = "Hello world"
var body: some View {
TextEditor(text: $text)
}
}
答案 11 :(得分:2)
您可以只使用 TextEditor(text: $text)
,然后为诸如高度之类的内容添加任何修饰符。