How to make Swift Coordinator reflect parent's bindings changes? - swift

Let's say I have:
structure Document, which represents text document.
EditorView — an NSTextView, wrapped with Combine, which binds to Document.content<String>.
Document is a part of complex store:ObservableObject, so it can be bouneded to EditorView instance.
When I first create binding, it works as expected — editing NSTextView changes value in Document.content.
let document1 = Document(...)
let document2 = Document(...)
var editor = EditorView(doc: document1)
But if change binding to another Document...
editor.doc = document2
...then updateNSView can see new document2. But inside Coordiantor's textDidChange has still refrence to document1.
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.doc.content = textView.string
self.selectedRanges = textView.selectedRanges
}
So, initially, when i set new bindint, NSTextView changes it content to document2, but as I type, coordinator sends changes to document1.
Is it true, that Coordiantor keeps it's own copy of parent, and even if parent changes (#Binding doc is updated), it still references to old one?
How to make Coordinator reflect parent's bindings changes?
Thank you!
struct Document: Identifiable, Equatable {
let id: UUID = UUID()
var name: String
var content: String
}
struct EditorView: NSViewRepresentable {
#Binding var doc: Document
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> CustomTextView {
let textView = CustomTextView(
text: doc.content,
isEditable: isEditable,
font: font
)
textView.delegate = context.coordinator
return textView
}
func updateNSView(_ view: CustomTextView, context: Context) {
view.text = doc.content
view.selectedRanges = context.coordinator.selectedRanges
}
}
// MARK: - Coordinator
extension EditorView {
class Coordinator: NSObject, NSTextViewDelegate {
var parent: EditorView
var selectedRanges: [NSValue] = []
init(_ parent: EditorView) {
self.parent = parent
}
func textDidBeginEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.doc.content = textView.string
self.parent.onEditingChanged()
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.doc.content = textView.string
self.selectedRanges = textView.selectedRanges
}
func textDidEndEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.doc.content = textView.string
self.parent.onCommit()
}
}
}
// MARK: - CustomTextView
final class CustomTextView: NSView {
private var isEditable: Bool
private var font: NSFont?
weak var delegate: NSTextViewDelegate?
var text: String {
didSet {
textView.string = text
}
}
// ...

Is it true, that Coordiantor keeps it's own copy of parent, and even
if parent changes (#Binding doc is updated), it still references to
old one?
A parent, ie EditorView here, is struct, so answer is yes, in general it can be copied.
How to make Coordinator reflect parent's bindings changes?
Instead of (or additional to) parent, inject binding to Coordinator (via constructor) explicitly and work with it directly.

Related

Array Binding in NSViewRepresentable Updating Incorrectly After Sorting

I have a ForEach binding to an array of items. Each item has an index (which represents how it should be sorted), and some text. Each row in the ForEach has an NSTextView in an NSViewRepresentable (I can't use TextEditor for various reasons), which uses the binding for the item's text. However, after sorting the list, if I update the NSTextView, one of the other views gets the update as well:
Here's my code:
struct ListItem: Hashable, Identifiable {
var index: Int
var text: String
var id = UUID()
static let initialItems: [ListItem] = [
.init(index: 2, text: "String 2"),
.init(index: 0, text: "String 0"),
.init(index: 1, text: "String 1"),
]
}
struct ContentView: View {
#State var items = ListItem.initialItems
var body: some View {
Form {
ForEach($items) { item in
HStack {
TextField("Index: ", value: item.index, format: .number)
.textFieldStyle(.roundedBorder)
TVRep(text: item.text)
}
}
Button("Sort!") {
items.sort(by: {$0.index < $1.index})
}
}
.padding()
}
}
struct TVRep: NSViewRepresentable {
#Binding var text: String
func makeNSView(context: Context) -> NSTextView {
let view = NSTextView()
view.string = text
view.delegate = context.coordinator
return view
}
func updateNSView(_ nsView: NSTextView, context: Context) {
nsView.string = text
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, NSTextViewDelegate {
var parent: TVRep!
init(parent: TVRep) {
super.init()
self.parent = parent
}
func textDidChange(_ notification: Notification) {
guard let tv = notification.object as? NSTextView else {return}
parent.text = tv.string
}
}
}
This doesn't occur when using SwiftUI's built-in TextEditor. Interestingly, this only seems to happen when I update the text binding's value within the Representable view - if I don't have the delegate method updating the binding value, this doesn't occur.
I do know that if I call sorted(by:) in the ForEach argument that it will update properly, but that means that the moment that the TextField for the index is updated, the views are sorted, while I want to be able to sort them manually.

Refreshing PDFViewer on SwiftUI UIViewRepresentable

I have created a SwiftUI wrapper for creating and displaying PDFs. I have a couple of functionalities that output the new pdf as Data. I have a binding to my PDFViewer but it is not working as expected. The challenge comes when I want to refresh the view (for example I added new text, so the binding data changes) without calling 'updateUIView'. I would like to solve it without calling updateUIView as I would like to do not create PDFDocument(data: data) again if possible.
I have researched about delegates and did not find any 'update' or similar function.
I also tried layoutDocumentView without success
Would like a solution that refreshes the view without creating the doc again
Would also have access to the currentPage in a better way if possible (now I am working with Notifications)
struct PDFViewer: UIViewRepresentable {
typealias UIViewType = PDFView
#Binding var data: Data
#Binding var currentPageNumber: Int?
var pdfView: PDFView
let singlePage: Bool
init(pdfView: PDFView, data: Binding<Data>, singlePage: Bool = false, currentPage: Binding<Int?>) {
self.pdfView = pdfView
self._data = data
self.singlePage = singlePage
self._currentPageNumber = currentPage
}
func makeUIView(context: UIViewRepresentableContext<PDFViewer>) -> UIViewType {
pdfView.autoScales = true
if singlePage {
pdfView.displayMode = .singlePage
}
pdfView.delegate = context.coordinator
pdfView.document = PDFDocument(data: data) // <- DO NOT REFRESH EVEN IF DATA CHANGES
NotificationCenter.default.addObserver(forName: .PDFViewSelectionChanged, object: nil, queue: nil) { (notification) in
DispatchQueue.main.async {
let newPage = (pdfView.currentPage?.pageRef!.pageNumber)!
print(newPage)
if currentPageNumber != newPage {
currentPageNumber = newPage
}
}
}
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context _: UIViewRepresentableContext<PDFViewer>) {
//// let newPDFDoc = PDFDocument(data: data) <---- DO NOT WANT TO CREATE IT AGAIN
// if pdfView.document?.dataRepresentation() != newPDFDoc?.dataRepresentation() {
//// pdfView.document = newPDFDoc
//// pdfView.go(to: pdfView.currentPage!)
// }
}
class Coordinator: NSObject, PDFViewDelegate, UIGestureRecognizerDelegate {
var parent: PDFViewer
init(_ parent: PDFViewer) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
you could try something very simple like this approach, to not refresh the view
even when data changes:
#State private var firstTimeOnly = true
....
func updateUIView(_ pdfView: UIViewType, context _: UIViewRepresentableContext<PDFViewer>) {
if self.firstTimeOnly {
self.firstTimeOnly = false
let newPDFDoc = PDFDocument(data: data) // <---- DO NOT WANT TO CREATE IT AGAIN
if pdfView.document?.dataRepresentation() != newPDFDoc?.dataRepresentation() {
pdfView.document = newPDFDoc
pdfView.go(to: pdfView.currentPage!)
}
}
}
Similarly in makeUIView.

SwiftUI Binding Edits Wrong TextField after Item Reordered

Xcode 13 beta 5, iOS 14, macOS 11.6
I have a parent SwiftUI view that lists some children. Each child is bound to an NSViewRepresentable. Everything works and I can edit the values as expected. But once I reorder the items in the list and edit a field, it edits the wrong field. It appears that the binding remains intact from the previous item order.
Here's what that looks like:
Here's the parent:
struct ParentView: View {
#StateObject var model = ThingModel.shared
var body: some View {
VStack{
ForEach($model.things){ $thing in
ChildView(thing: $thing)
//Reorder
.onDrag{
model.draggedThing = thing
return NSItemProvider(object: NSString())
}
}
Text("Value: \(model.value)").font(.title)
}
.frame(width:300, height: 200)
}
}
...and here's the child view:
struct ChildView: View {
#Binding var thing: Thing
#StateObject var model = ThingModel.shared
var body: some View{
HStack{
GrowingField(text: $thing.text, submit: {
model.value = thing.text
print(thing.text)
})
Text(" = ")
.opacity(0.4)
}
.padding(10)
.onDrop(of: [UTType.text], delegate: ThingReorderDelegate(hoveredThing: thing))
}
}
Last of all, here is the NSViewRepresentable which is called GrowingField. For simplicity, I have omitted the NSTextField subclass.
struct GrowingField: NSViewRepresentable{
#Binding var text: String
var submit:(() -> Void)? //Hit enter
func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField()
textField.delegate = context.coordinator
textField.stringValue = text
return textField
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.stringValue = text
context.coordinator.textBinding = $text
}
//Delegates
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, NSTextFieldDelegate {
let parent: GrowingField
var textBinding : Binding<String>?
init(_ field: GrowingField) {
self.parent = field
}
func controlTextDidChange(_ obj: Notification) {
guard let textField = obj.object as? NSTextField else { return }
self.textBinding?.wrappedValue = textField.stringValue
}
//Listen for certain keyboard keys
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
switch commandSelector{
case #selector(NSStandardKeyBindingResponding.insertNewline(_:)):
//- Enter -
parent.submit?()
textView.window?.makeFirstResponder(nil) //Blur cursor
return true
default:
return false
}
}
}
}
Why does the binding to the NSViewRepresentable not follow the field after it is reordered?
Here is a sample project to download and try it out.
I believe the issue (bug?) is with the ForEach-generated binding.
If you forego the generated binding and create your own, everything seems to work as expected.
Added to the ThingModel:
func bindingForThing(id: String) -> Binding<Thing> {
.init {
self.things.first { $0.id == id }!
} set: { newThing in
self.things = self.things.map { $0.id == id ? newThing : $0 }
}
}
And the ParentView:
ForEach(model.things){ thing in
ChildView(thing: model.bindingForThing(id: thing.id))

How to make text typed in TextField undeletable?

I am Fairly new to programming, after looking around I thought that id take my chances with asking here. I am basically needing for text typed in a TextField to be undeletable, although additional text can be added/typed.
A different approach would be to create a custom keybaord without a delete key, although I couldn't find a good starting place as in research and etc for doing so in SwiftUI.
I have a basic TextField setup with an empty Binding<String>
Looking for pointers of what I should research and or learn.
Thank you.
The idea is the create UITextField class and use UIViewRepresentable to bind with SwiftUI view. By this, you can use all delegate methods and detect backspace. Also, using this you can prevent from cut and delete from tap action.
UndeletableTextField custom class
class UndeletableTextField: UITextField {
// This for prevent to cut and delete
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(UIResponderStandardEditActions.delete(_:)) ||
action == #selector(UIResponderStandardEditActions.cut(_:)) {
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
UIViewRepresentable view
struct UndeletableTextFieldUI: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: Context) -> UndeletableTextField {
let textField = UndeletableTextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UndeletableTextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: UndeletableTextFieldUI
init(parent: UndeletableTextFieldUI) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Here we detect backspace and ignore it.
if let char = string.cString(using: String.Encoding.utf8) {
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Backspace was pressed")
return false
}
}
return true
}
}
}
ContentView
struct ContentView: View {
#State private var text: String = ""
var body: some View {
UndeletableTextFieldUI(text: $text, placeholder: "Type here")
}
}
You will probably want a custom binding for that String. The following is a super basic example -- you'll probably want to cover more edge cases. Note that I've chosen to include the logic in an ObservableObject, but you could do the same in a View struct by changing _textStore to be a #State variable. You'd also want to include logic for initial text, etc.
class ViewModel : ObservableObject {
var _textStore = ""
var textBinding : Binding<String> {
Binding<String>(get: {
return _textStore
}, set: { newValue in
//do something here to compare newValue to what existed before
//note that this solution will allow text to be both prepended and appended to the existing text
if _textStore.contains(newValue) { _textStore = newValue }
})
}
}
...
#ObservedObject var vm = ViewModel()
TextField("", vm.textBinding)

Handle keyboard inputs in NSTextView embedded via NSViewRepresentable? (SwiftUI/MacOS)

I'm new to SwiftUI and am utterly confused.. I managed to embed a NSTextView into my SwiftUI View and bind its text with the below code.
What I don't understand; is there a way to handle keyboard inputs to the NSTextView and change its text accordingly (e.g. CMD + R sets the text color of the selected text to red)? Is there even any way to interact with UI-Elements in SwiftUI?
"RichTextField"
struct RichTextField: NSViewRepresentable {
typealias NSViewType = NSTextView
#Binding var attributedString: NSAttributedString
func makeNSView(context: Context) -> NSTextView {...
// [...]
}
View
struct EditWindow: View {
#ObservedObject var model: EditEntryViewModel
#Binding var isPresented: Bool
var body: some View {
RichTextField(attributedString: self.$model.answer1, isEditable: true)
// [...]
}
}
Furthermore, I've managed to set up a menu command in the AppDelegate, but how could I use this to change the text (at a certain position) in a NSTextView of an arbitrary View?
#IBAction func setTagImportant(_ sender: Any) {
print("setTagImportant")
}
Thanks a lot for shedding some light on this for me...
Ironically, immediately after finally posting this question, I found a solution; simply subclass the NSTextView and then override keyDown:
import SwiftUI
class RichTextFieldExtended: NSTextView {
override func keyDown(with event: NSEvent) {
if event.modifierFlags.contains(NSEvent.ModifierFlags.command) {
switch event.keyCode {
case 18: // 1
print("1 PRESSED")
default:
print("keyCode \(event.keyCode) wasn't handled")
super.keyDown(with: event)
}
} else {
super.keyDown(with: event)
}
}
}
Then include the subclassed NSTextView in the NSViewRepresentable, as follows
struct RichTextField: NSViewRepresentable {
typealias NSViewType = RichTextFieldExtended
#Binding var attributedString: NSAttributedString
var isEditable: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> RichTextFieldExtended {
let textView = RichTextFieldExtended(frame: .zero)
textView.textStorage?.setAttributedString(self.attributedString)
textView.isEditable = isEditable
textView.delegate = context.coordinator
textView.translatesAutoresizingMaskIntoConstraints = false
textView.autoresizingMask = [.width, .height]
return textView
}
func updateNSView(_ nsView: RichTextFieldExtended, context: Context) {
// nsView.textStorage!.setAttributedString(self.attributedString)
}
// Source: https://medium.com/fantageek/use-xib-de9d8a295757
class Coordinator: NSObject, NSTextViewDelegate {
let parent: RichTextField
init(_ RichTextField: RichTextField) {
self.parent = RichTextField
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? RichTextFieldExtended else { return }
self.parent.attributedString = textView.attributedString()
}
}
}
Cheers