diff --git a/iOS/PostEditor/MultilineTextView.swift b/iOS/PostEditor/MultilineTextView.swift index ffffa18..54c79b1 100644 --- a/iOS/PostEditor/MultilineTextView.swift +++ b/iOS/PostEditor/MultilineTextView.swift @@ -1,160 +1,160 @@ // Credit: https://stackoverflow.com/a/58639072 import SwiftUI import UIKit private struct UITextViewWrapper: UIViewRepresentable { typealias UIViewType = UITextView @Binding var text: String @Binding var calculatedHeight: CGFloat @Binding var isEditing: Bool var textStyle: UIFont var onDone: (() -> Void)? func makeUIView(context: UIViewRepresentableContext) -> UITextView { let textField = UITextView() textField.delegate = context.coordinator textField.isEditable = true textField.isSelectable = true textField.isUserInteractionEnabled = true textField.isScrollEnabled = false textField.backgroundColor = UIColor.clear textField.smartDashesType = .no if nil != onDone { textField.returnKeyType = .next } textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return textField } func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { if uiView.text != self.text { uiView.text = self.text } let font = textStyle let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle) uiView.font = fontMetrics.scaledFont(for: font) if uiView.window != nil && isEditing { - DispatchQueue.main.async { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.55) { uiView.becomeFirstResponder() } } UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight) } fileprivate static func recalculateHeight(view: UIView, result: Binding) { 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, isFirstResponder: $isEditing, onDone: onDone) } final class Coordinator: NSObject, UITextViewDelegate { @Binding var isFirstResponder: Bool var text: Binding var calculatedHeight: Binding var onDone: (() -> Void)? init( text: Binding, height: Binding, isFirstResponder: Binding, onDone: (() -> Void)? = nil ) { self.text = text self.calculatedHeight = height self._isFirstResponder = isFirstResponder 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 } func textViewDidEndEditing(_ textView: UITextView) { self.isFirstResponder = false } } } struct MultilineTextField: View { private var placeholder: String private var textStyle: UIFont private var onCommit: (() -> Void)? @Binding var isFirstResponder: Bool @Binding private var text: String private var internalText: Binding { Binding(get: { self.text }) { // swiftlint:disable:this multiple_closures_with_trailing_closure self.text = $0 self.showingPlaceholder = $0.isEmpty } } @State private var dynamicHeight: CGFloat = 100 @State private var showingPlaceholder = false init ( _ placeholder: String = "", text: Binding, font: UIFont, isFirstResponder: Binding, onCommit: (() -> Void)? = nil ) { self.placeholder = placeholder self.onCommit = onCommit self.textStyle = font self._isFirstResponder = isFirstResponder self._text = text self._showingPlaceholder = State(initialValue: self.text.isEmpty) } var body: some View { UITextViewWrapper( text: self.internalText, calculatedHeight: $dynamicHeight, isEditing: $isFirstResponder, textStyle: textStyle, onDone: onCommit ) .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight) .background(placeholderView, alignment: .topLeading) } var placeholderView: some View { Group { if showingPlaceholder { let font = Font(textStyle) Text(placeholder).foregroundColor(.gray) .padding(.leading, 4) .padding(.top, 8) .font(font) } } } } diff --git a/iOS/PostEditor/PostTextEditingView.swift b/iOS/PostEditor/PostTextEditingView.swift index 7184105..0482f7d 100644 --- a/iOS/PostEditor/PostTextEditingView.swift +++ b/iOS/PostEditor/PostTextEditingView.swift @@ -1,84 +1,87 @@ import SwiftUI struct PostTextEditingView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @ObservedObject var post: WFAPost @Binding var updatingTitleFromServer: Bool @Binding var updatingBodyFromServer: Bool @State private var appearance: PostAppearance = .serif @State private var titleTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 26)! @State private var titleIsFirstResponder: Bool = true @State private var bodyTextStyle: UIFont = UIFont(name: "Lora-Regular", size: 17)! @State private var bodyIsFirstResponder: Bool = false private let lineSpacingMultiplier: CGFloat = 0.5 private let textEditorHeight: CGFloat = 50 init( post: ObservedObject, updatingTitleFromServer: Binding, updatingBodyFromServer: Binding ) { self._post = post self._updatingTitleFromServer = updatingTitleFromServer self._updatingBodyFromServer = updatingBodyFromServer UITextView.appearance().backgroundColor = .clear } var body: some View { ScrollView(.vertical) { MultilineTextField( "Title (optional)", text: $post.title, font: titleTextStyle, isFirstResponder: $titleIsFirstResponder, onCommit: didFinishEditingTitle ) .accessibilityLabel(Text("Title (optional)")) .accessibilityHint(Text("Add or edit the title for your post; use the Return key to skip to the body")) .onChange(of: post.title) { _ in if post.status == PostStatus.published.rawValue && !updatingTitleFromServer { post.status = PostStatus.edited.rawValue } if updatingTitleFromServer { updatingTitleFromServer = false } } MultilineTextField( "Write...", text: $post.body, font: bodyTextStyle, isFirstResponder: $bodyIsFirstResponder ) .accessibilityLabel(Text("Body")) .accessibilityHint(Text("Add or edit the body of your post")) .onChange(of: post.body) { _ in if post.status == PostStatus.published.rawValue && !updatingBodyFromServer { post.status = PostStatus.edited.rawValue } if updatingBodyFromServer { updatingBodyFromServer = false } } } - .onChange(of: titleIsFirstResponder, perform: { _ in - self.bodyIsFirstResponder.toggle() + .onChange(of: titleIsFirstResponder, perform: { value in + self.bodyIsFirstResponder = !value }) .onAppear(perform: { switch post.appearance { case "sans": self.appearance = .sans case "wrap", "mono", "code": self.appearance = .mono default: self.appearance = .serif } self.titleTextStyle = UIFont(name: appearance.rawValue, size: 26)! self.bodyTextStyle = UIFont(name: appearance.rawValue, size: 17)! }) + .onDisappear { + hideKeyboard() + } } private func didFinishEditingTitle() { self.titleIsFirstResponder = false self.bodyIsFirstResponder = true } }