Swift UITapGestureRecognizer not calling - swift

Pretty simple problem that is making out to be harder to solve than it should: My gesture is simple not calling, at all. I am using a uiviewrepresentable that is displayed inside of a zstack. If i add a .tapgesture{} to CameraView() directly it works just fine. But i need to get the tap position
public struct CameraView: UIViewRepresentable {
#EnvironmentObject var ue: UserEvents
public func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
let focusGesture = UITapGestureRecognizer(target: self, action: #selector(context.coordinator.tapFocus(_:)))
self.ue.cameraPreview = AVCaptureVideoPreviewLayer(session: ue.session)
self.ue.cameraPreview.frame = view.frame
self.ue.cameraPreview.videoGravity = ue.videoGravity
self.ue.session.startRunning()
view.isUserInteractionEnabled = true
view.layer.addSublayer(self.ue.cameraPreview)
focusGesture.numberOfTapsRequired = 1
view.addGestureRecognizer(focusGesture)
return view
}
public func updateUIView(_ uiView: UIViewType, context: Context) { }
public func makeCoordinator() -> Self.Coordinator {
return Coordinator()
}
public class Coordinator: NSObject {
#objc public func tapFocus(_ sender: UITapGestureRecognizer) {
print("tap")
}
}
}

The target should be the coordinator (which is a persistent entity, unlike the transient View), not self.
let focusGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.tapFocus(_:)))

Related

Why does a binding in UIViewRepresentables Coordinator have a constant read value

I have been writing a UIViewRepresentable and noticing some curios effects in regards to a binding I'm passing into the view.
When I read the bindings value in the coordinator through the saved UIViewRepresentable the value is always the value that it was initialized with. Trying to update the same binding however triggers an update in the surrounding UI.
This is code produces this behavior:
struct NativeTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.borderStyle = .roundedRect
view.addTarget(
context.coordinator,
action: #selector(Coordinator.updateText(sender:)),
for: .editingChanged
)
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
context.coordinator.updateUI(uiView)
}
func makeCoordinator() -> Coordinator {
Coordinator(_text)
}
class Coordinator: NSObject {
#Binding var text: String
init(_ text: Binding<String>){
_text = text
}
#objc func updateText(sender: UITextField){
text=sender.text!
}
func updateUI(_ uiView: UITextField) {
uiView.text = text
}
}
}
If I hover give my updateUI method a NativeTextView parameter, and use the .text field of it through the parameter, I read the correct value and the UI works correctly:
struct NativeTextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.borderStyle = .roundedRect
view.addTarget(
context.coordinator,
action: #selector(Coordinator.updateText(sender:)),
for: .editingChanged
)
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
context.coordinator.updateUI(uiView, view: self)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var myView: NativeTextView
init(_ view: NativeTextView){
self.myView=view
}
#objc func updateText(sender: UITextField){
myView.text=sender.text!
}
func updateUI(_ uiView: UITextField, view: NativeTextView) {
uiView.text = view.text
}
}
}
It seems that the binding retains the ability to write to the outside #State variable but does not manage to access the current states value correctly. I'm guessing that this has something to do with the recreation of the NativeTextView view when SwiftUI notices an update of the #State, but I have not been able to find any documentation that would explain this behavior.
Does anyone know why this happens?
PS: for completeness this is my ContentViews body:
ZStack {
Color.red
VStack {
Text(test)
.padding()
.onTapGesture() {
test = "Bla"
}
NativeTextView(text: $test)
}
}

Changing height of NSSearchField in SwiftUI

I am working with SwiftUI 1.0.
I have created a search bar for SwiftUI as the following:
import SwiftUI
struct Searchbar: NSViewRepresentable {
class Coordinator: NSObject, NSSearchFieldDelegate {
var parent: Searchbar
init(_ parent: Searchbar) {
self.parent = parent
}
func controlTextDidChange(_ notification: Notification) {
guard let searchField = notification.object as? NSSearchField else {
log.error("Unexpected control in update notification", source: .ui)
return
}
self.parent.search = searchField.stringValue
}
}
#Binding var search: String
func makeNSView(context: Context) -> NSSearchField {
let searchfield = NSSearchField(frame: .zero)
return searchfield
}
func changeSearchFieldItem(searchfield: NSSearchField, sender: AnyObject) -> NSSearchField {
//Based on the Menu item selection in the search field the placeholder string is set
(searchfield.cell as? NSSearchFieldCell)?.placeholderString = sender.title
return searchfield
}
func updateNSView(_ searchField: NSSearchField, context: Context) {
searchField.stringValue = search
searchField.delegate = context.coordinator
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
}
This is working fine so far when using it in my View:
Searchbar(search: $searchText)
I am wondering if the height of the NSSearchField can be changed to have a view similar to what is seen in the Maps.app:
Update: You can also set the controlSize to .large if you’re on Big Sur.
https://developer.apple.com/documentation/appkit/nscontrol/controlsize/large
You can add a height constraint:
func makeNSView(context: Context) -> NSSearchField {
let searchfield = NSSearchField(frame: .zero)
searchfield.translatesAutoresizingMaskIntoConstraints = false
searchfield.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true
return searchfield
}
…which works in macOS 11.1 Big Sur. Unfortunately the Focus ring does not adapt its height. You could hide it like this:
searchTextField.focusRingType = .none
… but that does not seem desirable in most situations.

How to make ContextMenu in NewView:NSViewRepresentable?

I have an old OldNSView : NSView, which is embedded in SwiftUI NewView<S:SomeProtocol> : NSViewRepresentable struct. And I need to add a context menu. But when I try to create NSMenuItems for oldNsView.menu, they asks for #Selector, which needs #objc func ... to work. Because it's inside SwiftUI View compiler doesn't allow to do it. (With no #selectors defined I can see menu items labels (of course they are greyed-out and they do nothing).
On the second hand, If I try to add .contextMenu to SwiftUI newView nothing happens. No menu at all. It works with Text(...) or another generic views but not with custom NSViewRepresentable struct.
All mouseUp(with event..), mouseDown(), and mouseDragged() call super.mouse..(with event)
if I use Coordinator
public class Coordinator: NSObject {
...
//Compiler does't like if (_ sender: OldView).
//It likes (_ sender: NSView), but in this case context menu is grayed-out
#objc func changeXAxis(_ sender: NSView) {
if let view = sender as? OldView {
print ("changeXAxis")
}
}
}
Is there a way to do it?
You have coordinator concept in representable to handle interaction with NSView.
Here is simple demo. Tested with Xcode 11.4 / macOS 10.15.5
struct DemoViewWithMenu: NSViewRepresentable {
func makeNSView(context: Context) -> NSView {
let view = NSView()
let menu = NSMenu()
let item = menu.addItem(withTitle: "Do Action", action: #selector(Coordinator.action(_:)), keyEquivalent: "")
item.target = context.coordinator
view.menu = menu
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject {
#objc func action(_ sender: Any) {
print(">> do action here")
}
}
}

View content doesn't appear in UIScrollView

I have this...
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.addSubview(UIHostingController(rootView: preview).view)
return control
}
If I put my preview view outside of my custom scroll view then I can see it. But when I add it as a subview of a UIScrollView then I can't see anything.
Have I added it correctly?
Here is the complete code for the scroll view, which I got from here.
import SwiftUI
import Foundation
struct LegacyScrollView : UIViewRepresentable {
var preview: AnyView
init(preview: AnyView) {
self.preview = preview
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.addSubview(UIHostingController(rootView: preview).view)
return control
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
}
class Coordinator: NSObject {
var control: LegacyScrollView
init(_ control: LegacyScrollView) {
self.control = control
}
#objc func handleRefreshControl(sender: UIRefreshControl) {
sender.endRefreshing()
}
}
}

Understanding UIViewRepresentable

Swift 5.0 iOS 13
Trying to understand how UIViewRepresentable works, and put together this simple example, almost there, but maybe its complete nonsense. Yes, I know there is already a tapGesture in SwiftUI, this is just a test.
Won't compile cause it says 'super.init' isn't called on all paths before returning from initialiser, which I try and set but obviously not correctly.
import SwiftUI
struct newView: UIViewRepresentable {
typealias UIViewType = UIView
var v = UIView()
func updateUIView(_ uiView: UIView, context: Context) {
v.backgroundColor = UIColor.yellow
}
func makeUIView(context: Context) -> UIView {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(Coordinator.handleTap(sender:)))
v.addGestureRecognizer(tapGesture)
return v
}
func makeCoordinator() -> newView.Coordinator {
Coordinator(v)
}
final class Coordinator: UIView {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func handleTap(sender: UITapGestureRecognizer) {
print("tap")
}
}
}
Just make your Coordinator is a NSObject, it usually plays bridge/controller/delegate/actor role, but not presentation, so should not be is-a-UIView
final class Coordinator: NSObject {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
and one more...
func makeUIView(context: Context) -> UIView {
// make target a coordinator, which is already present in context !!
let tapGesture = UITapGestureRecognizer(target: context.coordinator,
action: #selector(Coordinator.handleTap(sender:)))
v.addGestureRecognizer(tapGesture)
return v
}
Thats because your Coordinator is a subclass of the UIView and you
Must call a designated initializer of the superclass 'UIView'
before returning from the init:
init(_ view: UIView) {
self.view = view
super.init(frame: .zero) // Or any other frame you need
}