Files
2026-05-17 10:33:07 -04:00

135 lines
4.5 KiB
Swift

// The MIT License (MIT)
//
// Copyright (c) 2020-2026 Alexander Grebenyuk (github.com/kean).
#if os(iOS) || os(visionOS)
import SwiftUI
import CoreData
import Pulse
import Combine
@available(iOS 18, tvOS 18, macOS 15, watchOS 11, visionOS 1, *)
public struct RichTextView: View {
@ObservedObject var viewModel: RichTextViewModel
var isTextViewBarItemsHidden = false
@State private var shareItems: ShareItems?
@State private var isWebViewOpen = false
@Environment(\.textViewSearchContext) private var searchContext
public init(viewModel: RichTextViewModel) {
self.viewModel = viewModel
}
public func textViewBarItemsHidden(_ isHidden: Bool) -> RichTextView {
var copy = self
copy.isTextViewBarItemsHidden = isHidden
return copy
}
public var body: some View {
contents
.onAppear { viewModel.prepare(searchContext) }
.navigationBarItems(trailing: navigationBarTrailingItems)
.sheet(item: $shareItems, content: ShareView.init)
.sheet(isPresented: $isWebViewOpen) {
NavigationView {
WebView(data: viewModel.textStorage.string.data(using: .utf8) ?? Data(), contentType: "application/html")
.inlineNavigationTitle("Browser Preview")
.navigationBarItems(trailing: Button(action: {
isWebViewOpen = false
}) { Image(systemName: "xmark") })
}
}
}
@ViewBuilder
private var contents: some View {
ContentView(viewModel: viewModel)
.searchable(text: $viewModel.searchTerm)
.disableAutocorrection(true)
}
private struct ContentView: View {
@ObservedObject var viewModel: RichTextViewModel
@Environment(\.isSearching) private var isSearching
var body: some View {
WrappedTextView(viewModel: viewModel)
.edgesIgnoringSafeArea([.top, .bottom])
.overlay {
if isSearching || !viewModel.matches.isEmpty {
if #available(iOS 26, visionOS 26, *) {
VStack {
Spacer()
HStack {
Spacer()
RichTextViewSearchToobar(viewModel: viewModel)
}
.padding(.horizontal, 12)
.padding(.bottom, 8)
}
} else {
VStack {
Spacer()
HStack {
Spacer()
RichTextViewSearchToobar(viewModel: viewModel)
.padding()
}
}
}
}
}
}
}
@ViewBuilder
private var navigationBarTrailingItems: some View {
if !isTextViewBarItemsHidden {
Menu(content: {
AttributedStringShareMenu(shareItems: $shareItems) {
viewModel.textStorage
}
}, label: {
Image(systemName: "square.and.arrow.up")
})
// TODO: This should be injected/added outside of the text view
if viewModel.contentType?.isHTML ?? false {
Menu(content: {
Section {
if viewModel.contentType?.isHTML == true {
Button(action: { isWebViewOpen = true }) {
Label("Open in Browser", systemImage: "safari")
}
}
}
}, label: {
Image(systemName: "ellipsis.circle")
})
}
}
}
}
#if DEBUG
@available(iOS 18, tvOS 18, macOS 15, watchOS 11, visionOS 1, *)
#Preview {
NavigationView {
RichTextView(viewModel: makePreviewViewModel())
.inlineNavigationTitle("Rich Text View")
}
}
private func makePreviewViewModel() -> RichTextViewModel {
let json = try! JSONSerialization.jsonObject(with: MockJSON.allPossibleValues)
let string = TextRenderer().render(json: json)
return RichTextViewModel(string: string, contentType: "application/json")
}
#endif
#endif