diff --git a/iOS/PostEditor/PostBodyTextView.swift b/iOS/PostEditor/PostBodyTextView.swift index e9b071d..b287618 100644 --- a/iOS/PostEditor/PostBodyTextView.swift +++ b/iOS/PostEditor/PostBodyTextView.swift @@ -1,111 +1,134 @@ // Based on https://stackoverflow.com/a/56508132 and https://stackoverflow.com/a/48360549 import SwiftUI class PostBodyCoordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate { @Binding var text: String @Binding var isFirstResponder: Bool var lineSpacingMultiplier: CGFloat var didBecomeFirstResponder: Bool = false var postBodyTextView: PostBodyTextView weak var textView: UITextView? init( _ textView: PostBodyTextView, text: Binding, isFirstResponder: Binding, lineSpacingMultiplier: CGFloat ) { self.postBodyTextView = textView _text = text _isFirstResponder = isFirstResponder self.lineSpacingMultiplier = lineSpacingMultiplier + + super.init() + + updateSize() + } + + func updateSize() { + DispatchQueue.main.async { + guard let view = self.textView else { return } + let size = view.sizeThatFits(view.bounds.size) + if self.postBodyTextView.height != size.height { + self.postBodyTextView.height = size.height + } + } } func textViewDidChange(_ textView: UITextView) { DispatchQueue.main.async { self.postBodyTextView.text = textView.text ?? "" } } func textViewDidEndEditing(_ textView: UITextView) { self.isFirstResponder = false self.didBecomeFirstResponder = false } func layoutManager( _ layoutManager: NSLayoutManager, + didCompleteLayoutFor textContainer: NSTextContainer?, + atEnd layoutFinishedFlag: Bool + ) { + updateSize() + } + + func layoutManager( + _ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect ) -> CGFloat { // HACK: - This seems to be the only way to get line spacing to update dynamically on iPad // when switching between full-screen, split-screen, and slide-over views. if let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first { // Get the width of the window to determine the size class if window.frame.width < 600 { // Use 0.25 multiplier for compact size class return 17 * 0.25 } else { // Use 0.5 multiplier otherwise return 17 * 0.5 } } else { return 17 * lineSpacingMultiplier } } } struct PostBodyTextView: UIViewRepresentable { @Binding var text: String @Binding var textStyle: UIFont + @Binding var height: CGFloat @Binding var isFirstResponder: Bool @State var lineSpacing: CGFloat func makeUIView(context: UIViewRepresentableContext) -> UITextView { let textView = UITextView() textView.isEditable = true textView.isUserInteractionEnabled = true textView.isScrollEnabled = true textView.alwaysBounceVertical = false textView.smartDashesType = .no context.coordinator.textView = textView textView.delegate = context.coordinator textView.layoutManager.delegate = context.coordinator let font = textStyle let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle) textView.font = fontMetrics.scaledFont(for: font) textView.backgroundColor = UIColor.clear return textView } func makeCoordinator() -> PostBodyCoordinator { return Coordinator( self, text: $text, isFirstResponder: $isFirstResponder, lineSpacingMultiplier: lineSpacing ) } func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { if uiView.text != text { uiView.text = text } let font = textStyle let fontMetrics = UIFontMetrics(forTextStyle: .largeTitle) uiView.font = fontMetrics.scaledFont(for: font) // We don't want the text field to become first responder every time SwiftUI refreshes the view. if isFirstResponder && !context.coordinator.didBecomeFirstResponder { uiView.becomeFirstResponder() context.coordinator.didBecomeFirstResponder = true } } } diff --git a/iOS/PostEditor/PostTextEditingView.swift b/iOS/PostEditor/PostTextEditingView.swift index 51f5ed2..57241c5 100644 --- a/iOS/PostEditor/PostTextEditingView.swift +++ b/iOS/PostEditor/PostTextEditingView.swift @@ -1,108 +1,119 @@ 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 titleTextHeight: CGFloat = 50 + @State private var bodyTextHeight: CGFloat = 50 @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 titleFieldHeight: CGFloat { - let minHeight: CGFloat = 50 + let minHeight: CGFloat = textEditorHeight if titleTextHeight < minHeight { return minHeight } return titleTextHeight } + var bodyFieldHeight: CGFloat { + let minHeight: CGFloat = textEditorHeight + if bodyTextHeight < minHeight { + return minHeight + } + return bodyTextHeight + } var body: some View { - VStack { + ScrollView(.vertical) { ZStack(alignment: .topLeading) { if post.title.count == 0 { Text("Title (optional)") .font(Font(titleTextStyle)) .foregroundColor(Color(UIColor.placeholderText)) .padding(.horizontal, 4) .padding(.vertical, 8) .accessibilityHidden(true) } PostTitleTextView( text: $post.title, textStyle: $titleTextStyle, height: $titleTextHeight, isFirstResponder: $titleIsFirstResponder, lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier ) .accessibilityLabel(Text("Title (optional)")) .accessibilityHint(Text("Add or edit the title for your post; use the Return key to skip to the body")) .frame(height: titleFieldHeight) .onChange(of: post.title) { _ in if post.status == PostStatus.published.rawValue && !updatingTitleFromServer { post.status = PostStatus.edited.rawValue } if updatingTitleFromServer { updatingTitleFromServer = false } } } ZStack(alignment: .topLeading) { if post.body.count == 0 { Text("Write…") .font(Font(bodyTextStyle)) .foregroundColor(Color(UIColor.placeholderText)) .padding(.horizontal, 4) .padding(.vertical, 8) .accessibilityHidden(true) } PostBodyTextView( text: $post.body, textStyle: $bodyTextStyle, + height: $bodyTextHeight, isFirstResponder: $bodyIsFirstResponder, lineSpacing: horizontalSizeClass == .compact ? lineSpacingMultiplier / 2 : lineSpacingMultiplier ) + .frame(height: bodyFieldHeight) .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)! }) } }