diff --git a/iOS/PostEditor/MultilineTextView.swift b/iOS/PostEditor/MultilineTextView.swift index 54ac9e6..75bcc6b 100644 --- a/iOS/PostEditor/MultilineTextView.swift +++ b/iOS/PostEditor/MultilineTextView.swift @@ -1,135 +1,158 @@ // 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.font = UIFont.preferredFont(forTextStyle: .body) textField.isSelectable = true textField.isUserInteractionEnabled = true textField.isScrollEnabled = false textField.backgroundColor = UIColor.clear let font = textStyle let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle) textField.font = fontMetrics.scaledFont(for: font) if nil != onDone { - textField.returnKeyType = .done + 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 } - if uiView.window != nil, !uiView.isFirstResponder { + + if uiView.window != nil, isEditing { 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, onDone: onDone) + 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, onDone: (() -> Void)? = nil) { + 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, onCommit: (() -> Void)? = nil) { + 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 73e5549..7184105 100644 --- a/iOS/PostEditor/PostTextEditingView.swift +++ b/iOS/PostEditor/PostTextEditingView.swift @@ -1,68 +1,84 @@ 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) - .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( + "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 } - MultilineTextField("Write...", text: $post.body, font: bodyTextStyle) - .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 - } + 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() }) .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)! }) } + + private func didFinishEditingTitle() { + self.titleIsFirstResponder = false + self.bodyIsFirstResponder = true + } }