Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

Commit

Permalink
release c53c372c
Browse files Browse the repository at this point in the history
  • Loading branch information
runner authored and runner committed Feb 9, 2024
1 parent 7b34160 commit 3ad0308
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public struct WysiwygComposerView: View {

@ViewBuilder
private var placeholderView: some View {
if viewModel.isContentEmpty, viewModel.textView?.isDictationRunning != true {
if viewModel.isContentEmpty, !viewModel.textView.isDictationRunning {
Text(placeholder)
.font(Font(UIFont.preferredFont(forTextStyle: .body)))
.foregroundColor(placeholderColor)
Expand Down Expand Up @@ -121,15 +121,7 @@ struct UITextViewWrapper: UIViewRepresentable {
}

func makeUIView(context: Context) -> WysiwygTextView {
// Default text container have a slightly different behaviour
// than what iOS would use if textContainer is nil, this
// fixes issues with background color not working on newline characters.
let layoutManager = NSLayoutManager()
let textStorage = NSTextStorage()
let textContainer = NSTextContainer()
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
let textView = WysiwygTextView(frame: .zero, textContainer: textContainer)
let textView = WysiwygTextView()
// Assign the textView to the view model ASAP
viewModel.textView = textView
textView.accessibilityIdentifier = "WysiwygComposer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,21 @@ import UIKit
public class WysiwygComposerViewModel: WysiwygComposerViewModelProtocol, ObservableObject {
// MARK: - Public

/// The textView that the model currently manages.
public var textView: WysiwygTextView? {
/// The textView that the model currently manages, a default text view is provided, but you should always inject it from the UIWrapper
public lazy var textView = {
let textView = WysiwygTextView()
textView.linkTextAttributes[.foregroundColor] = parserStyle.linkColor
textView.mentionDisplayHelper = mentionDisplayHelper
textView.apply(attributedContent)
return textView
}() {
didSet {
guard let textView else {
return
}
textView.linkTextAttributes[.foregroundColor] = parserStyle.linkColor
textView.mentionDisplayHelper = mentionDisplayHelper
textView.apply(attributedContent)
}
}

/// The composer minimal height.
public let minHeight: CGFloat
/// The mention replacer defined by the hosting application.
Expand Down Expand Up @@ -77,7 +81,7 @@ public class WysiwygComposerViewModel: WysiwygComposerViewModelProtocol, Observa
public var parserStyle: HTMLParserStyle {
didSet {
// In case of a color change, this will refresh the attributed text
textView?.linkTextAttributes[.foregroundColor] = parserStyle.linkColor
textView.linkTextAttributes[.foregroundColor] = parserStyle.linkColor
let update = model.setContentFromHtml(html: content.html)
applyUpdate(update)
updateTextView()
Expand Down Expand Up @@ -113,6 +117,13 @@ public class WysiwygComposerViewModel: WysiwygComposerViewModelProtocol, Observa
return WysiwygComposerContent(markdown: model.getContentAsMessageMarkdown(),
html: model.getContentAsMessageHtml())
}

/// The mention helper that will be used by the underlying textView
public var mentionDisplayHelper: MentionDisplayHelper? {
didSet {
textView.mentionDisplayHelper = mentionDisplayHelper
}
}

// MARK: - Private

Expand Down Expand Up @@ -153,10 +164,7 @@ public class WysiwygComposerViewModel: WysiwygComposerViewModelProtocol, Observa
guard let self = self else { return }
// Improves a lot the user experience by keeping the selected range always visible when there are changes in the size.
DispatchQueue.main.async {
guard let textView = self.textView else {
return
}
textView.scrollRangeToVisible(textView.selectedRange)
self.textView.scrollRangeToVisible(self.textView.selectedRange)
}
}
.store(in: &cancellables)
Expand Down Expand Up @@ -221,7 +229,7 @@ public extension WysiwygComposerViewModel {
/// Clear the content of the composer.
func clearContent() {
if plainTextMode {
textView?.attributedText = NSAttributedString(string: "", attributes: defaultTextAttributes)
textView.attributedText = NSAttributedString(string: "", attributes: defaultTextAttributes)
updateCompressedHeightIfNeeded()
} else {
applyUpdate(model.clear())
Expand Down Expand Up @@ -283,10 +291,6 @@ public extension WysiwygComposerViewModel {

public extension WysiwygComposerViewModel {
func updateCompressedHeightIfNeeded() {
guard let textView else {
return
}

let idealTextHeight = textView
.sizeThatFits(CGSize(width: textView.bounds.size.width,
height: CGFloat.greatestFiniteMagnitude)
Expand All @@ -307,8 +311,8 @@ public extension WysiwygComposerViewModel {

// This fixes a bug where the attributed string keeps link and inline code formatting
// when they are the last formatting to be deleted
if textView?.attributedText.length == 0 {
textView?.typingAttributes = defaultTextAttributes
if textView.attributedText.length == 0 {
textView.typingAttributes = defaultTextAttributes
}

let update: ComposerUpdate
Expand Down Expand Up @@ -336,7 +340,7 @@ public extension WysiwygComposerViewModel {
case let .update(newState):
if newState[.link] != actionStates[.link] {
applyUpdate(update, skipTextViewUpdate: true)
textView?.apply(attributedContent)
textView.apply(attributedContent)
updateCompressedHeightIfNeeded()
return false
}
Expand All @@ -350,7 +354,7 @@ public extension WysiwygComposerViewModel {

func select(range: NSRange) {
do {
guard let text = textView?.attributedText, !plainTextMode else { return }
guard let text = textView.attributedText, !plainTextMode else { return }
let htmlSelection = try text.htmlRange(from: range)
Logger.viewModel.logDebug(["Sel(att): \(range)",
"Sel: \(htmlSelection)",
Expand All @@ -369,12 +373,10 @@ public extension WysiwygComposerViewModel {

func didUpdateText() {
if plainTextMode {
if let textView {
if textView.text.isEmpty != isContentEmpty {
isContentEmpty = textView.text.isEmpty
}
plainTextContent = textView.attributedText
if textView.text.isEmpty != isContentEmpty {
isContentEmpty = textView.text.isEmpty
}
plainTextContent = textView.attributedText
} else {
reconciliateIfNeeded()
applyPendingFormatsIfNeeded()
Expand Down Expand Up @@ -411,7 +413,7 @@ public extension WysiwygComposerViewModel {

@available(iOS 16.0, *)
func getIdealSize(_ proposal: ProposedViewSize) -> CGSize {
guard let textView, let width = proposal.width else { return .zero }
guard let width = proposal.width else { return .zero }

let idealHeight = textView
.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
Expand Down Expand Up @@ -443,7 +445,7 @@ private extension WysiwygComposerViewModel {
// Note: this makes replaceAll act like .keep on cases where we expect the text
// view to be properly updated by the system.
if !skipTextViewUpdate {
textView?.apply(attributedContent)
textView.apply(attributedContent)
updateCompressedHeightIfNeeded()
}
case let .select(startUtf16Codeunit: start,
Expand Down Expand Up @@ -549,7 +551,7 @@ private extension WysiwygComposerViewModel {
if let mentionReplacer {
attributed = mentionReplacer.postProcessMarkdown(in: attributed)
}
textView?.attributedText = attributed
textView.attributedText = attributed
updateCompressedHeightIfNeeded()
} else {
let update = model.setContentFromMarkdown(markdown: computeMarkdownContent())
Expand All @@ -562,7 +564,7 @@ private extension WysiwygComposerViewModel {
/// Reconciliate the content of the model with the content of the text view.
func reconciliateIfNeeded() {
do {
guard let textView, !textView.isDictationRunning,
guard !textView.isDictationRunning,
let replacement = try StringDiffer.replacement(from: attributedContent.text.htmlChars,
to: textView.attributedText.htmlChars) else {
return
Expand All @@ -586,7 +588,7 @@ private extension WysiwygComposerViewModel {
case StringDifferError.tooComplicated,
StringDifferError.insertionsDontMatchRemovals:
// Restore from the model, as otherwise the composer will enter a broken state
textView?.apply(attributedContent)
textView.apply(attributedContent)
updateCompressedHeightIfNeeded()
Logger.viewModel.logError(["Reconciliate failed, content has been restored from the model"],
functionName: #function)
Expand All @@ -606,7 +608,7 @@ private extension WysiwygComposerViewModel {
func applyPendingFormatsIfNeeded() {
guard hasPendingFormats else { return }

textView?.apply(attributedContent)
textView.apply(attributedContent)
updateCompressedHeightIfNeeded()
hasPendingFormats = false
}
Expand All @@ -615,9 +617,6 @@ private extension WysiwygComposerViewModel {
///
/// - Returns: A markdown string.
func computeMarkdownContent() -> String {
guard let textView else {
return ""
}
let markdownContent: String
if let mentionReplacer, let attributedText = textView.attributedText {
// `MentionReplacer` should restore altered content to valid markdown.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import UIKit

public protocol WysiwygComposerViewModelProtocol: AnyObject {
/// The textView that the model manages.
var textView: WysiwygTextView? { get set }
var textView: WysiwygTextView { get set }

/// Whether the current content of the composer is empty.
var isContentEmpty: Bool { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,21 @@ public class WysiwygTextView: UITextView {
/// Internal delegate for the text view.
weak var wysiwygDelegate: WysiwygTextViewDelegate?

public var mentionDisplayHelper: MentionDisplayHelper?

private let flusher = WysiwygPillsFlusher()

public var mentionDisplayHelper: MentionDisplayHelper?
public convenience init() {
// Default text container have a slightly different behaviour
// than what iOS would use if textContainer is nil, this
// fixes issues with background color not working on newline characters.
let layoutManager = NSLayoutManager()
let textStorage = NSTextStorage()
let textContainer = NSTextContainer()
textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)
self.init(frame: .zero, textContainer: textContainer)
}

override public init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ extension WysiwygComposerViewModelTests {

private extension WysiwygComposerViewModelTests {
func assertAutoCorrectEnabled() {
XCTAssertEqual(viewModel.textView?.autocorrectionType, .yes)
XCTAssertEqual(viewModel.textView.autocorrectionType, .yes)
}

func assertAutocorrectDisabled() {
XCTAssertEqual(viewModel.textView?.autocorrectionType, .no)
XCTAssertEqual(viewModel.textView.autocorrectionType, .no)
}
}
Loading

0 comments on commit 3ad0308

Please sign in to comment.