When opening RichTextView, show found results

This commit is contained in:
kean
2023-01-15 08:34:25 -05:00
parent f87dd7245f
commit d6ef2a7513
4 changed files with 94 additions and 20 deletions
+3 -1
View File
@@ -6,13 +6,15 @@ import SwiftUI
import Pulse
import PulseUI
#warning("TODO: update")
@main
struct Pulse_Demo_iOSApp: App {
var body: some Scene {
// let _ = testProxy()
WindowGroup {
NavigationView {
ConsoleView(store: .demo)
_SearchView()
// ConsoleView(store: .demo)
}
}
}
@@ -7,6 +7,19 @@ import Pulse
import CoreData
import Combine
#warning("TODO: remove public")
public struct _SearchView: View {
public init() {}
public var body: some View {
if #available(iOS 15, *) {
ConsoleSearchView(viewModel: .init(entities: try! LoggerStore.mock.allMessages()))
}
}
}
// TODO: use custom search bar?
@available(iOS 15, tvOS 15, *)
struct ConsoleSearchView: View {
@ObservedObject var viewModel: ConsoleSearchViewModel
@@ -20,7 +33,7 @@ struct ConsoleSearchView: View {
// TODO: when open body, start with a search term immediatelly
let occurences = Array(result.occurences.enumerated())
ForEach(occurences.prefix(3), id: \.offset) { item in
NavigationLink(destination: makeDestination(for: item.element.kind, entity: result.entity)) {
NavigationLink(destination: makeDestination(for: item.element, entity: result.entity)) {
makeCell(for: item.element)
}
}
@@ -38,23 +51,22 @@ struct ConsoleSearchView: View {
// TODO: add occurence IDs instead of indices
func makeCell(for occurence: ConsoleSearchOccurence) -> some View {
NavigationLink(destination: Text("Placeholder")) {
// TODO: handle errors
let attr = try! AttributedString(occurence.occurrence, including: \.uiKit)
VStack(alignment: .leading, spacing: 4) {
Text(occurence.kind.title + " (Line: \(occurence.line):\(occurence.range.lowerBound))")
.font(ConsoleConstants.fontTitle)
.foregroundColor(.secondary)
Text(attr)
.lineLimit(3)
}
// TODO: handle errors
let attr = try! AttributedString(occurence.occurrence, including: \.uiKit)
return VStack(alignment: .leading, spacing: 4) {
Text(occurence.kind.title + " (Line: \(occurence.line):\(occurence.range.lowerBound))")
.font(ConsoleConstants.fontTitle)
.foregroundColor(.secondary)
Text(attr)
.lineLimit(3)
}
}
func makeDestination(for kind: ConsoleSearchOccurence.Kind, entity: NSManagedObject) -> some View {
switch kind {
func makeDestination(for occurence: ConsoleSearchOccurence, entity: NSManagedObject) -> some View {
switch occurence.kind {
case .responseBody:
return NetworkInspectorResponseBodyView(viewModel: .init(task: entity as! NetworkTaskEntity))
.environment(\.textViewSearchContext, occurence.searchContext)
}
}
}
@@ -80,6 +92,11 @@ final class ConsoleSearchViewModel: ObservableObject {
$searchText.dropFirst().sink { [weak self] in
self?.search($0)
}.store(in: &cancellables)
#warning("TEPM")
DispatchQueue.main.async {
self.searchText = "Nuke"
}
}
// TODO: perform in background
@@ -110,7 +127,7 @@ final class ConsoleSearchViewModel: ObservableObject {
var occurences: [ConsoleSearchOccurence] = []
if let responseBody = task.responseBody?.data, let string = String(data: responseBody, encoding: .utf8) {
let matches = regex.matches(in: string)
for match in matches {
for (index, match) in matches.enumerated() {
let range = match.fullMatch.startIndex..<match.fullMatch.endIndex
// TODO: check range limits
// TODO: find next and current line + adjust to fit current line on screen
@@ -124,11 +141,14 @@ final class ConsoleSearchViewModel: ObservableObject {
// TODO: optimize + show line number
let range2 = Int.random(in: 1...50)..<200
// TODO: pass options
let context = RichTextViewModel.SearchContext(searchTerm: searchText, options: .default, matchIndex: index)
let occurence = ConsoleSearchOccurence(
kind: .responseBody,
line: .random(in: 1...100),
range: range2,
occurrence: substring
occurrence: substring,
searchContext: context
)
occurences.append(occurence)
}
@@ -157,6 +177,7 @@ struct ConsoleSearchOccurence {
let line: Int
let range: Range<Int>
let occurrence: NSAttributedString
let searchContext: RichTextViewModel.SearchContext
}
struct ConsoleSearchResultViewModel: Identifiable {
@@ -14,6 +14,7 @@ struct RichTextView: View {
let viewModel: RichTextViewModel
private var isTextViewBarItemsHidden = false
@Environment(\.textViewSearchContext) private var searchContext
init(viewModel: RichTextViewModel) {
self.viewModel = viewModel
@@ -26,6 +27,12 @@ struct RichTextView: View {
}
var body: some View {
contents
.onAppear { viewModel.prepare(searchContext) }
}
@ViewBuilder
private var contents: some View {
if #available(iOS 15, *) {
_RichTextView(viewModel: viewModel, isTextViewBarItemsHidden: isTextViewBarItemsHidden)
} else {
@@ -72,7 +79,7 @@ struct _RichTextView: View {
WrappedTextView(viewModel: viewModel)
.edgesIgnoringSafeArea([.bottom])
.overlay {
if isSearching {
if isSearching || !viewModel.matches.isEmpty {
VStack {
Spacer()
HStack {
@@ -306,3 +313,14 @@ struct RichTextView_Previews: PreviewProvider {
}
}
#endif
private struct TextViewSearchContextKey: EnvironmentKey {
static var defaultValue: RichTextViewModel.SearchContext?
}
extension EnvironmentValues {
var textViewSearchContext: RichTextViewModel.SearchContext? {
get { self[TextViewSearchContextKey.self] }
set { self[TextViewSearchContextKey.self] = newValue }
}
}
@@ -56,6 +56,22 @@ final class RichTextViewModel: ObservableObject {
}.store(in: &bag)
}
func prepare(_ context: SearchContext?) {
guard let context = context else { return }
// Not updated self.searchTerm because searchable doesn't like that
let matches = search(searchTerm: context.searchTerm, in: textStorage.string as NSString, options: context.options)
self.didUpdateMatches(matches)
if context.matchIndex < matches.count {
DispatchQueue.main.async {
self.textView?.layoutManager.allowsNonContiguousLayout = false // Remove this workaround
UIView.performWithoutAnimation {
self.updateMatchIndex(context.matchIndex)
}
}
}
}
func display(_ text: NSAttributedString) {
self.text = text
self.matches.removeAll()
@@ -127,7 +143,7 @@ final class RichTextViewModel: ObservableObject {
updateMatchIndex(selectedMatchIndex - 1 < 0 ? matches.count - 1 : selectedMatchIndex - 1)
}
private func updateMatchIndex(_ newIndex: Int) {
func updateMatchIndex(_ newIndex: Int) {
let previousIndex = selectedMatchIndex
selectedMatchIndex = newIndex
didUpdateCurrentSelectedMatch(previousMatch: previousIndex)
@@ -137,9 +153,20 @@ final class RichTextViewModel: ObservableObject {
guard !matches.isEmpty else { return }
// Scroll to visible range
// Make sure it's somewhere in the middle (find newlines)
var range = matches[selectedMatchIndex]
if range.length + 50 < textStorage.length {
range.length += 50
var index = range.upperBound
var newlines = 0
let string = textStorage.string as NSString
while index < textStorage.length {
if let character = UnicodeScalar(string.character(at: index)).map(Character.init), character.isNewline {
newlines += 1
range.length += index - range.upperBound
if newlines == 8 {
break
}
}
index += 1
}
if let textView = textView {
textView.scrollRangeToVisible(range)
@@ -167,6 +194,12 @@ final class RichTextViewModel: ObservableObject {
.foregroundColor: UXColor.white
], range: range)
}
struct SearchContext {
let searchTerm: String
let options: StringSearchOptions
let matchIndex: Int
}
}
private func search(searchTerm: String, in string: NSString, options: StringSearchOptions) -> [NSRange] {