Skip to content

Instantly share code, notes, and snippets.

@dlgchg
Created June 6, 2020 08:11
Show Gist options
  • Save dlgchg/3a9d89b82a844794a109d57f8b03d75c to your computer and use it in GitHub Desktop.
Save dlgchg/3a9d89b82a844794a109d57f8b03d75c to your computer and use it in GitHub Desktop.

Revisions

  1. dlgchg created this gist Jun 6, 2020.
    140 changes: 140 additions & 0 deletions MultilineTextField.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,140 @@
    //
    // MultilineTextField.swift
    // GipsyToDo
    //
    // Created by liwei on 2020/6/4.
    // Copyright © 2020 liwei. All rights reserved.
    //

    import Foundation
    import SwiftUI
    import UIKit

    struct MultilineTextField: View {

    private var placeholder: String
    private var onCommit: (() -> Void)?
    @State private var viewHeight: CGFloat = 40 //start with one line
    @State private var shouldShowPlaceholder = false
    @Binding private var text: String
    @Binding private var showKeyBoard: Bool

    private var internalText: Binding<String> {
    Binding<String>(get: { self.text } ) {
    self.text = $0
    self.shouldShowPlaceholder = $0.isEmpty
    }
    }

    var body: some View {
    UITextViewWrapper(text: self.internalText, showKeyBoard: $showKeyBoard, calculatedHeight: $viewHeight, onDone: onCommit)
    .frame(minHeight: viewHeight, maxHeight: viewHeight)
    .background(placeholderView, alignment: .topLeading)
    }

    var placeholderView: some View {
    Group {
    if shouldShowPlaceholder {
    Text(placeholder).foregroundColor(.gray)
    .padding(.leading, 4)
    .padding(.top, 8)
    }
    }
    }

    init (_ placeholder: String = "", text: Binding<String>, showKeyBoard: Binding<Bool>, onCommit: (() -> Void)? = nil) {
    self.placeholder = placeholder
    self.onCommit = onCommit
    self._text = text
    self._showKeyBoard = showKeyBoard
    self._shouldShowPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
    }

    }


    private struct UITextViewWrapper: UIViewRepresentable {
    typealias UIViewType = UITextView

    @Binding var text: String
    @Binding var showKeyBoard: Bool
    @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
    textField.backgroundColor = UIColor.clear


    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 self.showKeyBoard {
    uiView.becomeFirstResponder()
    } else {
    uiView.resignFirstResponder()
    }
    // if uiView.window != nil, !uiView.isFirstResponder {
    // uiView.becomeFirstResponder()
    // }
    UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
    }

    private 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 // call in next render cycle.
    }
    }
    }

    func makeCoordinator() -> Coordinator {
    return Coordinator(text: $text, showKeyBoard: $showKeyBoard, height: $calculatedHeight, onDone: onDone)
    }

    final class Coordinator: NSObject, UITextViewDelegate {
    var text: Binding<String>
    var showKeyBoard: Binding<Bool>
    var calculatedHeight: Binding<CGFloat>
    var onDone: (() -> Void)?

    init(text: Binding<String>,showKeyBoard: Binding<Bool>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
    self.text = text
    self.showKeyBoard = showKeyBoard
    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
    }
    }

    }