如何在SwiftUI中创建多行TextField?

时间:2019-06-06 06:30:12

标签: ios swiftui

我一直在尝试在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

但这是输出:

enter image description here

12 个答案:

答案 0 :(得分:25)

iOS 14

它称为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
}

演示

Demo


iOS 13

使用本机UITextView

您可以在带有此结构的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
        }
    }
}

优势:

  • 支持 iOS 13
  • 与旧代码共享
  • 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。提交时

Preview of swiftui multiline textfield with content fit Added placeholder

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

enter image description here

答案 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>
                                        &nbsp;&nbsp;&nbsp;
                                        <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),然后为诸如高度之类的内容添加任何修饰符。