From 3bd2a9ebae0bce1d7d3932f785b57af35eb157e5 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Wed, 27 Sep 2023 15:51:01 +0400 Subject: [PATCH] Auto growing code editor --- .../CodeEditor/View/SwiftUI/CodeEditor.swift | 10 +++++++ .../UIKit/CodeEditorView/CodeEditorView.swift | 8 ++++-- .../CodeEditorViewDelegate.swift | 1 + .../UIKit/CodeTextView/CodeTextView.swift | 26 +++++++++++++++++++ .../Sources/Modules/Step/StepAssembly.swift | 1 + .../Views/StepQuizCodeEditorView.swift | 23 +++++++++++----- .../Sources/Systems/KeyboardManager.swift | 4 +++ 7 files changed, 65 insertions(+), 8 deletions(-) diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/SwiftUI/CodeEditor.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/SwiftUI/CodeEditor.swift index c1a2713ea7..9a9c597101 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/SwiftUI/CodeEditor.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/SwiftUI/CodeEditor.swift @@ -28,6 +28,8 @@ struct CodeEditor: UIViewRepresentable { var onDidEndEditing: (() -> Void)? + var onDidChangeHeight: ((CGFloat) -> Void)? + // MARK: UIViewRepresentable static func dismantleUIView(_ uiView: CodeEditorView, coordinator: Coordinator) { @@ -36,6 +38,7 @@ struct CodeEditor: UIViewRepresentable { coordinator.onCodeDidChange = nil coordinator.onDidBeginEditing = nil coordinator.onDidEndEditing = nil + coordinator.onDidChangeHeight = nil coordinator.suggestionsPresentationContextProvider = nil } @@ -89,6 +92,7 @@ struct CodeEditor: UIViewRepresentable { onDidEndEditing?() } + context.coordinator.onDidChangeHeight = onDidChangeHeight } } @@ -104,6 +108,8 @@ extension CodeEditor { var onDidEndEditing: (() -> Void)? + var onDidChangeHeight: ((CGFloat) -> Void)? + init(suggestionsPresentationContextProvider: CodeEditorSuggestionsPresentationContextProviding?) { self.suggestionsPresentationContextProvider = suggestionsPresentationContextProvider } @@ -127,6 +133,10 @@ extension CodeEditor { ) -> UIViewController? { suggestionsPresentationContextProvider?.presentationController(for: codeEditorView) } + + func codeEditorViewDidChangeHeight(_ codeEditorView: CodeEditorView, height: CGFloat) { + onDidChangeHeight?(height) + } } } diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorView.swift index 367ad659af..bcfa8c4928 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorView.swift @@ -198,9 +198,13 @@ extension CodeEditorView: ProgrammaticallyInitializableViewProtocol { } } -// MARK: - CodeEditorView: UITextViewDelegate - +// MARK: - CodeEditorView: CodeTextViewDelegate - + +extension CodeEditorView: CodeTextViewDelegate { + func codeTextViewDidChangeHeight(_ textView: CodeTextView, height: CGFloat) { + delegate?.codeEditorViewDidChangeHeight(self, height: height) + } -extension CodeEditorView: UITextViewDelegate { func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { delegate?.codeEditorView(self, beginEditing: isEditable) return isEditable diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorViewDelegate.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorViewDelegate.swift index 1a001d82cf..36a317108a 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorViewDelegate.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorViewDelegate.swift @@ -6,6 +6,7 @@ protocol CodeEditorViewDelegate: AnyObject { func codeEditorViewDidBeginEditing(_ codeEditorView: CodeEditorView) func codeEditorViewDidEndEditing(_ codeEditorView: CodeEditorView) func codeEditorViewDidRequestSuggestionPresentationController(_ codeEditorView: CodeEditorView) -> UIViewController? + func codeEditorViewDidChangeHeight(_ codeEditorView: CodeEditorView, height: CGFloat) } extension CodeEditorViewDelegate { diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeTextView/CodeTextView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeTextView/CodeTextView.swift index 725e836688..33e951e877 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeTextView/CodeTextView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeTextView/CodeTextView.swift @@ -1,6 +1,10 @@ import Highlightr import UIKit +protocol CodeTextViewDelegate: UITextViewDelegate { + func codeTextViewDidChangeHeight(_ textView: CodeTextView, height: CGFloat) +} + extension CodeTextView { struct Appearance { var gutterWidth: CGFloat = 24 @@ -39,6 +43,10 @@ final class CodeTextView: UITextView { private lazy var codeTextViewLayoutManager = layoutManager as? CodeTextViewLayoutManager private lazy var codeAttributedString = textStorage as? CodeAttributedString + // Calculate textview's height + private var oldText: String = "" + private var oldSize: CGSize = .zero + var language: String? { didSet { guard language != oldValue, @@ -121,6 +129,7 @@ final class CodeTextView: UITextView { override func layoutSubviews() { super.layoutSubviews() codeTextViewLayoutManager?.appearance.currentLineWidth = bounds.width + calculateBestFitsSize() } override func draw(_ rect: CGRect) { @@ -231,6 +240,23 @@ final class CodeTextView: UITextView { return UIColor(red: 1.0 - r, green: 1.0 - g, blue: 1.0 - b, alpha: 1) } + + private func calculateBestFitsSize() { + guard bounds.size.width > 0, + let delegate = delegate as? CodeTextViewDelegate else { + return + } + + if text == oldText && bounds.size == oldSize { + return + } + + oldText = text + oldSize = bounds.size + + let size = sizeThatFits(CGSize(width: bounds.size.width, height: CGFloat.greatestFiniteMagnitude)) + delegate.codeTextViewDidChangeHeight(self, height: size.height) + } } // MARK: - CodeTextView: NSLayoutManagerDelegate - diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/StepAssembly.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/StepAssembly.swift index 6190473d29..d7fc9a77c7 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/StepAssembly.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/StepAssembly.swift @@ -42,6 +42,7 @@ final class StepAssembly: Assembly, UIKitAssembly { panModalPresenter: PanModalPresenter() ) let hostingController = StyledHostingController(rootView: stepView, appearance: .withoutBackButtonTitle) + hostingController.hidesBottomBarWhenPushed = true // Fixes an issue with that SwiftUI view content layout unexpectedly pop/jumps on appear hostingController.navigationItem.largeTitleDisplayMode = .never diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCode/Views/StepQuizCodeEditorView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCode/Views/StepQuizCodeEditorView.swift index bb49334921..83ef803910 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCode/Views/StepQuizCodeEditorView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCode/Views/StepQuizCodeEditorView.swift @@ -3,7 +3,7 @@ import SwiftUI extension StepQuizCodeEditorView { struct Appearance { let codeEditorInsets = LayoutInsets(vertical: LayoutInsets.defaultInset) - let codeEditorMinHeightHeight: CGFloat = 300 + let codeEditorMinHeight: CGFloat = 300 } } @@ -19,6 +19,8 @@ struct StepQuizCodeEditorView: View { @Environment(\.isEnabled) private var isEnabled + @State private var height: CGFloat = 300 + var body: some View { VStack(spacing: 0) { Divider() @@ -59,12 +61,21 @@ struct StepQuizCodeEditorView: View { codeTemplate: codeTemplate, language: language, isEditable: true, - textInsets: appearance.codeEditorInsets.uiEdgeInsets - ) - .frame( - maxWidth: .infinity, - minHeight: appearance.codeEditorMinHeightHeight + textInsets: appearance.codeEditorInsets.uiEdgeInsets, + onDidChangeHeight: { newHeight in + let constrainMinimumHeight = max(newHeight, appearance.codeEditorMinHeight) + guard constrainMinimumHeight != height else { + return + } + + DispatchQueue.main.async { + height = constrainMinimumHeight + KeyboardManager.reloadLayoutIfNeeded() + } + } ) + .frame(height: height) + .frame(maxWidth: .infinity) Divider() } diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Systems/KeyboardManager.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Systems/KeyboardManager.swift index b51604953d..7e909788db 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Systems/KeyboardManager.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Systems/KeyboardManager.swift @@ -22,4 +22,8 @@ enum KeyboardManager { static func setEnabled(_ isEnabled: Bool) { IQKeyboardManager.shared.enable = isEnabled } + + static func reloadLayoutIfNeeded() { + IQKeyboardManager.shared.reloadLayoutIfNeeded() + } }