Disable button if PKCanvasView is empty - swift

I'm building a format using SwiftUI on which a user has to draw a signature, and must not be allowed to move forward if the PKCanvasView is empty. I'm trying to disable the next button by using: .disabled(pkCanvasView.drawing.bounds.isEmpty)but this condition always return true so the button is always disable. But if I try the following, it seems the condition is working.
Button(text: "SAVE") {
if pkCanvasView.drawing.bounds.isEmpty {
showAlert = true //CONDITION WORKS
} else {
//Do something else
}
}
So this way an alert shows if the canvas is empty, but in this case I cannot use an alert, the button has to be disable. Any ideas on how I can achive this by using the .disabled()property?
EDIT:
This is how I wrapped PKCanvasView in a UIViewRepresentable
struct SignatureCanvas: UIViewRepresentable {
#Binding var canvas: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvas.drawingPolicy = .anyInput
canvas.tool = PKInkingTool(.pen, color: .black, width: 10)
return canvas
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
func checkIfEmpty() -> Bool {
if canvas.drawing.strokes.isEmpty {
return true
} else {
return false
}
}
}

You need to implement the coordinator pattern with a PKCanvasView delegate.
struct SignatureCanvas: UIViewRepresentable {
#Binding var canvas: PKCanvasView
#Binding var flag: Bool
//Create the coordinator and assign it to the context
func makeCoordinator() -> Coordinator {
Coordinator(flag: $flag)
}
func makeUIView(context: Context) -> PKCanvasView {
canvas.drawingPolicy = .anyInput
canvas.tool = PKInkingTool(.pen, color: .black, width: 10)
//assign coordinator as delegate
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) {}
func checkIfEmpty() -> Bool {
if canvas.drawing.strokes.isEmpty {
return true
} else {
return false
}
}
}
// the coordinator storing the binding and implementing the delegate
class Coordinator: NSObject {
#Binding var flag: Bool
init(flag: Binding<Bool>) {
self._flag = flag
}
}
extension Coordinator: PKCanvasViewDelegate {
//gets called when the drawing changes
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
//update the binding
flag = canvasView.drawing.strokes.isEmpty
}
}
In your ContentView
VStack {
SignatureCanvas(canvas: $canvas, flag: $flag)
Button(text: "SAVE") {
}
.disabled(flag)
}
And of course you need the appropriate state variable in Contentview
#State private var flag: Bool = true

Related

Swift: TextField clearing itself after isSecureTextEntry set

I'm having some difficulty implementing a custom textfield in SwiftUI. I'm trying to create a password field, however I'm having to dip into UIKit because I need to react when the field is focused, and (unlike with the standard TextField in SwiftUI) there is no onEditingChanged closure with SecureField.
So I have the following :
struct PasswordField: UIViewRepresentable {
#ObservedObject var viewModel: TextFieldFloatingWithBorderViewModel
func makeUIView(context: UIViewRepresentableContext<PasswordField>) -> UITextField {
let tf = UITextField(frame: .zero)
tf.isUserInteractionEnabled = true
tf.delegate = context.coordinator
return tf
}
func makeCoordinator() -> PasswordField.Coordinator {
return Coordinator(viewModel: viewModel)
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = viewModel.text
uiView.isSecureTextEntry = !viewModel.isRevealed
}
class Coordinator: NSObject, UITextFieldDelegate {
#ObservedObject var viewModel: TextFieldFloatingWithBorderViewModel
init(viewModel: TextFieldFloatingWithBorderViewModel) {
self.viewModel = viewModel
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.viewModel.text = textField.text ?? ""
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.viewModel.isFocused = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.viewModel.isFocused = false
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return false
}
}
}
We then declare this field as follows within a SwiftUI View:
HStack {
PasswordField(viewModel: viewModel)
Button(action: {
viewModel.toggleReveal()
}) {
viewModel.revealIcon
.foregroundColor(viewModel.isFocused ? .blue : viewModel.hasWarning ? .red: .gray)
}
}
In the TextFieldFloatingWithBorderViewModel viewModel we have a Published var isRevealed and then the following method called when the button is tapped:
func toggleReveal() {
isRevealed.toggle()
}
Because in the PasswordField we have:
uiView.isSecureTextEntry = !viewModel.isRevealed
This toggles the password field between secure view (i.e. input masked with dots) and standard view. This is working well, except that when the user toggles back to hidden and continues to type the password is being wiped:
I cannot work out why the password is being wiped here, and only when it goes from non secure to secure (not the other way around)
You already have the #FocusState for this. You can use it like any state variable. Here is some code where I put a yellow background on a TextField and conditionally controlled it with the #FocusState variable:
struct HighlightWhenFocused: View {
#State private var password: String = ""
#FocusState var passwordFocused: Bool
var body: some View {
PasswordView(password: $password)
.focused($passwordFocused)
.background(
Color.yellow
.opacity(passwordFocused ? 1 : 0)
)
}
}
struct PasswordView: View {
#Binding var password: String
#State private var secured = true
var body: some View {
HStack{
Text("Password:")
if secured{
SecureField("",text:$password)
}
else{
TextField("",text: $password)
}
Button {
secured.toggle()
} label: {
Image(systemName: secured ? "eye.slash" : "eye")
}
.buttonStyle(BorderlessButtonStyle())
}
}
}
Edited it to make it a secure password solution. Please note that when you switch between viewable/non-viewable, the fields lose focus because you tapped outside of them.

Cycle detected through use of UIKit Textfield Representable in SwiftUI

I am using a boolean (searchVM.showSearchView: Bool) to simultaneously turn opacity of a SearchView to 1.0 AND show/hide a UIRepresentableKeyboard.
This gives me the same functionality as Google Maps when you tap on the search bar and (simultaneously) the white search view appears, textfield focused, and keyboard showing.
However I am getting the below message print twice every time I set the showSearchView to true.
=== AttributeGraph: cycle detected through attribute 320536 ===
=== AttributeGraph: cycle detected through attribute 318224 ===
Why am I getting this message? Is it the passing around of my searchVM?
SearchViewModel
class SearchViewModel: ObservableObject {
var text: String = ""
#Published var showSearchView: Bool = false
}
Textfield & Cancel button
struct SearchBar: View {
#ObservedObject var searchVM: SearchViewModel
#State var text: String = ""
var body: some View {
HStack {
FirstResponderTextfield(searchVM: searchVM, text: $text, placeholder: "Search")
Text("Cancel")
.onTapGesture {
searchVM.showSearchView.toggle()
}
}
}
}
FirstResponderTextField
struct FirstResponderTextfield: UIViewRepresentable {
#ObservedObject var searchVM : SearchViewModel
#Binding var text: String
let placeholder: String
// COORDINATOR
class TextFieldCoordinator: NSObject, UITextFieldDelegate {
var control: FirstResponderTextfield
#Binding var text: String // unused but required
func textFieldDidChangeSelection(_ textField: UITextField) {
control.searchVM.text = textField.text ?? ""
}
func textFieldDidEndEditing(_ textField: UITextField) {
}
init(control: FirstResponderTextfield, text: Binding<String>) {
self.control = control
self._text = text
}
}
func makeCoordinator() -> TextFieldCoordinator {
return TextFieldCoordinator(control: self, text: $text)
}
func makeUIView(context: Context) -> some UIView {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if searchVM.showSearchView {
uiView.becomeFirstResponder() // show keyboard
} else {
uiView.resignFirstResponder() // dismiss keyboard
}
}
}
I fixed this by adding DispatchQueue.asyncAfter( .now() + 0.05). Probably a mickey-mouse way of doing things but it works!
func updateUIView(_ uiView: UIViewType, context: Context) {
// here we check to see if our textfield has become first responder
if searchVM.showSearchView {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
uiView.becomeFirstResponder() // show keyboard
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
uiView.resignFirstResponder() // dismiss keyboard
}
}
}

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))

NSTextField Changing Focus Order With Tab Key

I have a macOS SwiftUI app that displays a lot of text fields in a grid. Originally I was using SwiftUI TextField's, but they have the problem that you cannot set the focus order of them (the order they receive focus when you press the tab button). I need to change the focus order from horizontal row-by-row, to vertical column-by-column. There does not appear to be an easy way to do this in SwiftUI.
I found a solution for iOS here, that I tried to modify for mac. The problem is that the delegate functions are not getting called. My current code is below. How do I fix this?
Edit: I've updated my code in light of some of the comments, but the problem still remains the same: no delegate methods are being called.
import SwiftUI
struct OrderedTextField: NSViewRepresentable {
#Binding var text: String
#Binding var selectedField: Int
var tag: Int
func makeNSView(context: NSViewRepresentableContext<OrderedTextField>) -> NSTextField {
let textField = NSTextField()
textField.delegate = context.coordinator
textField.tag = tag
textField.placeholderString = ""
return textField
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
func updateNSView(_ nsView: NSTextField, context: NSViewRepresentableContext<OrderedTextField>) {
context.coordinator.newSelection = { newSelection in
DispatchQueue.main.async {
self.selectedField = newSelection
}
}
if nsView.tag == self.selectedField {
nsView.becomeFirstResponder()
}
}
}
extension OrderedTextField {
class Coordinator: NSObject, NSTextFieldDelegate {
#Binding var text: String
var newSelection: (Int) -> () = { _ in }
init(text: Binding<String>) {
print("Initializing!")
_text = text
}
func textShouldBeginEditing(_ textObject: NSText) -> Bool {
print("Should begin editing!")
return true
}
func textDidBeginEditing(_ notification: Notification) {
print("Began editing")
}
func textDidChange(_ notification: Notification) {
print("textDidChange")
}
func textShouldEndEditing(_ textObject: NSText) -> Bool {
print("should end editing")
return true
}
func textDidEndEditing(_ notification: Notification) {
print("did end editing")
}
}
}

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