I am classifying images per frame from ARSession delegate by Vision framework and CoreML in an Augmented Reality app, with ARKit and RealityKit. While processing a frame.capturedImage I am not requesting another frame.capturedImage for performance.
The camera is not giving smooth experience, it gets stuck time to time. Seems like a frame loss.
And I am getting this Warning:
[Session] ARSession <0x122cc3710>: ARSessionDelegate is retaining 14 ARFrames. This can lead to future camera frames being dropped.
My Codes:
import Foundation
import SwiftUI
import RealityKit
import ARKit
import CoreML
struct ARViewContainer: UIViewRepresentable {
var errorFunc: ()->Void
var frameUpdateFunc: ()->Void
#Binding var finalLabel:String
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal,.vertical]
config.environmentTexturing = .automatic
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh){
config.sceneReconstruction = .mesh
}
arView.session.delegate = context.coordinator
arView.session.run(config)
context.coordinator.myView = arView
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(finalLabel: $finalLabel, self, funct: self.errorFunc, frameUpdateFunc: self.frameUpdateFunc)
}
class Coordinator: NSObject, ARSessionDelegate {
var objectDetectionService = ObjectDetectionService()
var myView:ARView?
#Binding var finalLabel:String
var parent: ARViewContainer
var efunc:()->Void
var frameUpdateFunc:()->Void
var isLoopShouldContinue = true
var lastLocation: SCNVector3?
//let model = try? MobileNetV2(configuration: .init())
private let classifier = VisionClasifier(mlModel: try? MobileNetV2(configuration: .init()).model)
private var currentBuffer: CVPixelBuffer? = nil
init(finalLabel:Binding<String>,_ arView: ARViewContainer,funct: #escaping ()->Void, frameUpdateFunc:#escaping ()->Void) {
parent = arView
self.efunc = funct
self.frameUpdateFunc = frameUpdateFunc
_finalLabel = finalLabel
}
func session(_ session: ARSession, didFailWithError error: Error) {
//print("Error Tanvir: ",error)
self.efunc()
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
if isLoopShouldContinue{
self.classifyFrame(currentFrame: frame)
}
let transform = SCNMatrix4(frame.camera.transform)
let orientation = SCNVector3(-transform.m31, -transform.m32, transform.m33)
let location = SCNVector3(transform.m41, transform.m42, transform.m43)
let currentPositionOfCamera = orientation + location
if let lastLocation = lastLocation {
let speed = (lastLocation - currentPositionOfCamera).length()
isLoopShouldContinue = speed < 0.0025
}
lastLocation = currentPositionOfCamera
}
// When ARKit detects a new anchor, it will add it to the ARSession
// Whenever there is a newly added ARAnchor, you will get that anchor here.
// In this short tutorial, we will target the ARPlaneAnchor, and use the information stored
// in that anchor for visualization.
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
guard let myView = myView else {
return
}
for anchor in anchors {
if anchor is ARPlaneAnchor {
let planeAnchor = anchor as! ARPlaneAnchor
//addPlaneEntity(with: planeAnchor, to: myView)
}
}
}
// ARKit will automatically track and update the ARPlaneAnchor.
// We use that anchor to update the `skin` of the plane.
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
guard let myView = myView else {
return
}
for anchor in anchors {
if anchor is ARPlaneAnchor {
let planeAnchor = anchor as! ARPlaneAnchor
//updatePlaneEntity(with: planeAnchor, in: myView)
}
}
}
// When ARKit remove an anchor from the ARSession, you will get the removed
// anchor here.
func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
guard let myView = myView else {
return
}
for anchor in anchors {
if anchor is ARPlaneAnchor {
let planeAnchor = anchor as! ARPlaneAnchor
//removePlaneEntity(with: planeAnchor, from: myView)
}
}
}
func addAnnotation(rectOfInterest rect: CGRect, text: String,width:Float,height:Float) {
let point = CGPoint(x: rect.midX, y: rect.midY)
print("point:", point)
//let scnHitTestResults = myView.hitTest(point,
// options: [SCNHitTestOption.searchMode: SCNHitTestSearchMode.all.rawValue])
//guard !scnHitTestResults.contains(where: { $0.node.name == BubbleNode.name }) else { return }
let raycastResult = myView!.raycast(from: point, allowing: .estimatedPlane, alignment: .any)
// guard let raycastQuery = myView!.raycastQuery(from: point,
// allowing: .existingPlaneInfinite,
// alignment: .horizontal),
// let raycastResult = myView.session.raycast(raycastQuery).first else { return }
guard let raycastResult = raycastResult.first else{
print("raycast result failed")
return
}
let anchorExists = myView!.scene.anchors.contains(where: {$0.name == text})
guard anchorExists == false else{
print("anchor Already exists")
return
}
let position = raycastResult.worldTransform.columns.3
let myEntity = create2dEntity(with: position, boundingBox: rect, raycastResult: raycastResult,width:width ,height:height)
let planeAnchorEntity = AnchorEntity()
planeAnchorEntity.name = text
planeAnchorEntity.position = simd_make_float3(position)
planeAnchorEntity.addChild(myEntity)
// Finally, add the entity to scene.
myView!.scene.addAnchor(planeAnchorEntity)
print("anchor added: ", planeAnchorEntity.name)
}
func classifyFrame(currentFrame:ARFrame){
//let currentImageName = photos[currentIndex]
// 2
// 3
print("inside Classify")
//print("CurrentBuffer", currentBuffer)
guard self.currentBuffer == nil else {
//print("CurrentBuffer: ",currentBuffer)
//self.finalLabel = "current buffer problem"
return
}
self.currentBuffer = currentFrame.capturedImage
// guard let model = self.model else {
// return "Model not Found."
// }
let img = CIImage(cvImageBuffer: currentFrame.capturedImage)
let cgImage = convertCIImageToCGImage(inputImage: img)
guard let cgImage = cgImage else{
print("can not convert CGImage")
self.finalLabel = "can not convert CGImage"
return
}
objectDetectionService.detect(on: .init(pixelBuffer: currentFrame.capturedImage)) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let response):
self.finalLabel = response.classification.description
print("Real Width: ",response.boundingBox.width)
let rectOfInterest = VNImageRectForNormalizedRect(
response.boundingBox,
Int(self.myView!.bounds.width),
Int(self.myView!.bounds.height))
self.addAnnotation(rectOfInterest: rectOfInterest, text: response.classification.description,width: Float(response.boundingBox.width),height: Float(response.boundingBox.height))
print("Success:",response.classification.description)
self.currentBuffer = nil
case .failure(let error):
self.finalLabel = "Detection Failed"
print("Detection failure: ",error.localizedDescription)
self.currentBuffer = nil
break
}
}
}
}
}
func convertCIImageToCGImage(inputImage: CIImage) -> CGImage? {
let context = CIContext(options: nil)
if let cgImage = context.createCGImage(inputImage, from: inputImage.extent) {
return cgImage
}
return nil
}
// The ARPlaneAnchor contains the information we need to create the `skin` of the plane.
func addPlaneEntity(with anchor: ARPlaneAnchor, to view: ARView) {
let planeAnchorEntity = AnchorEntity(.plane([.any],
classification: [.any],
minimumBounds: [0.01, 0.01]))
let planeModelEntity = createPlaneModelEntity(with: anchor)
// Give Entity a name for tracking.
planeAnchorEntity.name = anchor.identifier.uuidString + "_anchor"
planeModelEntity.name = anchor.identifier.uuidString + "_model"
// Add ModelEntity as a child of AnchorEntity.
// AnchorEntity handles `position` of the plane.
// ModelEntity handles the `skin` of the plane.
planeAnchorEntity.addChild(planeModelEntity)
// Finally, add the entity to scene.
view.scene.addAnchor(planeAnchorEntity)
}
func create2dEntity(with position: simd_float4, boundingBox: CGRect, raycastResult:ARRaycastResult, width:Float,height:Float ) -> ModelEntity{
var planeMesh: MeshResource
var color: UIColor
print("horizotal plane")
color = UIColor.red.withAlphaComponent(0.5)
print("Constant width: 0.1 but BoundingBox Width: ",boundingBox.width)
planeMesh = .generatePlane(width: 0.1, height: 0.1)
return ModelEntity(mesh: planeMesh, materials: [SimpleMaterial(color: color, roughness: 0.25, isMetallic: false)])
}
func createPlaneModelEntity(with anchor: ARPlaneAnchor) -> ModelEntity {
var planeMesh: MeshResource
var color: UIColor
if anchor.alignment == .horizontal {
print("horizotal plane")
color = UIColor.blue.withAlphaComponent(0.5)
planeMesh = .generatePlane(width: anchor.extent.x, depth: anchor.extent.z)
} else if anchor.alignment == .vertical {
print("vertical plane")
color = UIColor.yellow.withAlphaComponent(0.5)
planeMesh = .generatePlane(width: anchor.extent.x, height: anchor.extent.z)
} else {
fatalError("Anchor is not ARPlaneAnchor")
}
return ModelEntity(mesh: planeMesh, materials: [SimpleMaterial(color: color, roughness: 0.25, isMetallic: false)])
}
func removePlaneEntity(with anchor: ARPlaneAnchor, from arView: ARView) {
guard let planeAnchorEntity = arView.scene.findEntity(named: anchor.identifier.uuidString+"_anchor") else { return }
arView.scene.removeAnchor(planeAnchorEntity as! AnchorEntity)
}
func updatePlaneEntity(with anchor: ARPlaneAnchor, in view: ARView) {
var planeMesh: MeshResource
guard let entity = view.scene.findEntity(named: anchor.identifier.uuidString+"_model") else { return }
let modelEntity = entity as! ModelEntity
if anchor.alignment == .horizontal {
planeMesh = .generatePlane(width: anchor.extent.x, depth: anchor.extent.z)
} else if anchor.alignment == .vertical {
planeMesh = .generatePlane(width: anchor.extent.x, height: anchor.extent.z)
} else {
fatalError("Anchor is not ARPlaneAnchor")
}
modelEntity.model!.mesh = planeMesh
}
import SceneKit
extension SCNVector3 {
func length() -> Float {
return sqrtf(x * x + y * y + z * z)
}
}
func -(l: SCNVector3, r: SCNVector3) -> SCNVector3 {
return SCNVector3Make(l.x - r.x, l.y - r.y, l.z - r.z)
}
func +(l: SCNVector3, r: SCNVector3) -> SCNVector3 {
return SCNVector3(l.x + r.x, l.y + r.y, l.z + r.z)
}
func /(l: SCNVector3, r: Float) -> SCNVector3 {
return SCNVector3(l.x / r, l.y / r, l.z / r)
}
Detection: (Here is the problem, I guess, in detect method)
import Foundation
import UIKit
import CoreML
import Vision
import SceneKit
class ObjectDetectionService {
var mlModel = try! VNCoreMLModel(for: YOLOv3Int8LUT().model)
//let model = try? YOLOv3Int8LUT(configuration: .init())
lazy var coreMLRequest: VNCoreMLRequest = {
return VNCoreMLRequest(model: mlModel,
completionHandler: self.coreMlRequestHandler)
}()
private var completion: ((Result<Response, Error>) -> Void)?
func detect(on request: Request, completion: #escaping (Result<Response, Error>) -> Void) {
self.completion = completion
//let orientation = .up
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: request.pixelBuffer)
do {
try imageRequestHandler.perform([coreMLRequest])
} catch {
self.complete(.failure(error))
return
}
}
}
private extension ObjectDetectionService {
func coreMlRequestHandler(_ request: VNRequest?, error: Error?) {
if let error = error {
complete(.failure(error))
return
}
guard let request = request, let results = request.results as? [VNRecognizedObjectObservation] else {
complete(.failure(RecognitionError.resultIsEmpty))
return
}
guard let result = results.first(where: { $0.confidence > 0.8 }),
let classification = result.labels.first else {
complete(.failure(RecognitionError.lowConfidence))
return
}
let response = Response(boundingBox: result.boundingBox,
classification: classification.identifier)
complete(.success(response))
}
func complete(_ result: Result<Response, Error>) {
DispatchQueue.main.async {
self.completion?(result)
self.completion = nil
}
}
}
enum RecognitionError: Error {
case unableToInitializeCoreMLModel
case resultIsEmpty
case lowConfidence
}
extension ObjectDetectionService {
struct Request {
let pixelBuffer: CVPixelBuffer
}
struct Response {
let boundingBox: CGRect
let classification: String
}
}
Why am I getting this warning, and How to get the camera smooth experience?
The session(_ session: ARSession, didUpdate frame: ARFrame) delegate method is called very frequently: many times per second. If your classifyFrame method is doing too much work, it will retain the ARFrame object until after the next frame is delivered to the delegate.
ARKit will warn you when too many frames are retained, typically because a queue is blocked in your delegate.
var str:String = "When I was ###young$$$, I thought ###time$$$ was money"
VStsck{
Text(str)
}
enter image description here
You can parse your string using regular expression and build Text based on found ranges:
struct HighlightedText: View{
let text: Text
private static let regularExpression = try! NSRegularExpression(
pattern: "###(?<content>((?!\\$\\$\\$).)*)\\$\\$\\$"
)
private struct SubstringRange {
let content: NSRange
let full: NSRange
}
init(_ string: String) {
let ranges = Self.regularExpression
.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.count)
)
.map { match in
SubstringRange(
content: match.range(withName: "content"),
full: match.range(at: 0)
)
}
var nextNotProcessedSymbol = 0
var text = Text("")
let nsString = string as NSString
func appendSubstringStartingNextIfNeeded(until endLocation: Int) {
if nextNotProcessedSymbol < endLocation {
text = text + Text(nsString.substring(
with: NSRange(
location: nextNotProcessedSymbol,
length: endLocation - nextNotProcessedSymbol
)
))
}
}
for range in ranges {
appendSubstringStartingNextIfNeeded(until: range.full.location)
text = text + Text(nsString.substring(with: range.content))
.foregroundColor(Color.red)
nextNotProcessedSymbol = range.full.upperBound
}
appendSubstringStartingNextIfNeeded(until: string.count)
self.text = text
}
var body: some View {
text
}
}
Usage:
HighlightedText("When I was ###young$$$, I thought ###time$$$ was money")
func hilightText(str:String) -> Text{
var resultText:Text = Text("")
if(str.contains("###")){
let titleArr:Array<String> = str.components(spearatedBy:"###")
for(title in titleArr){
resultText = resultText
+ Text(title.components(spearatedBy:"$$$")[0]).foregroundColor(Color.red)
+ Text(title.components(spearatedBy:"$$$")[1]).foregroundColor(Color.gray)
}else{
resultText = resultText + Text(title).foregroundColor(Color.gray)
}
}
return resultText
}else{
return Text(str).foregroundColor(Color.gray)
}
struct View1:View{
#State var title = "When I was ###young$$$, I thought ###time$$$ was money"
var body : some View{
hilightText(str:title)
}
}
Hello i am trying to make a shop page on my application. The shop page contains a table with a title that is clickable so it expands into subcategories of that title. Etc click coffee and get some different kind of coffee to buy from.
Title = Drinks, Sandwiches & Drinks in the picture below.
subcategory from title = Ham & cheese, Chicken, bacon & curry .... and so on.
Now the expanding and closing the titles work fine. But when i press + (add iteem to cart) i get a out out of index if it's clicked in a specific order.
if i expand sandwiches (like it is in the picture) and then expand drinks (the one below sandwiches) and then click on the plus on any subcategory in sandwiches and then close sandwiches again and press on plus on any subcategory in Drinks (the one below the now closed sandwiches) the application will crash with a out of index error. Only this combination gives this error.
How the code works:
/* Create Cells */
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -
> UITableViewCell {
// Row is DefaultCell
if let rowData = destinationData?[indexPath.row] {
let defaultCell = Bundle.main.loadNibNamed("TableViewCellMenuItems", owner: self, options: nil)?.first as! TableViewCellMenuItems
// tableView.dequeueReusableCell(withIdentifier: "TableViewCellMenuItems", for: indexPath).first as! TableViewCellMenuItems
defaultCell.menuName.text = rowData.name
defaultCell.menuPicture.image = rowData.image
defaultCell.menuFoldPic.image = #imageLiteral(resourceName: "ic_keyboard_arrow_down")
defaultCell.selectionStyle = .none
return defaultCell
}
// Row is ExpansionCell
else {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: indexPath.row)] {
// print("her åbner vi")
// // Create an ExpansionCell
let expansionCell =
Bundle.main.loadNibNamed("TableViewCellMenuItemExpanded", owner: self, options: nil)?.first as! TableViewCellMenuItemExpanded
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: indexPath.row)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = indexPath.row - parentCellIndex - 1
// Set the cell's data
expansionCell.itemPrice.text = String (describing: "\(rowData.menuItems![flightIndex].price) kr")
expansionCell.itemName.text = rowData.menuItems?[flightIndex].name
expansionCell.itemCount.text = String (describing: rowData.menuItems![flightIndex].count)
// expansionCell.itemCount.text = String (describing: indexPath.row)
expansionCell.itemAdd.tag = Int(indexPath.row)
expansionCell.itemMinus.tag = Int(indexPath.row)
expansionCell.itemAdd.addTarget(self, action: #selector(HomeSelectedShopViewController.plus(_:)), for: .touchUpInside)
expansionCell.itemMinus.addTarget(self, action: #selector(HomeSelectedShopViewController.minus(_:)), for: .touchUpInside)
expansionCell.selectionStyle = .none
return expansionCell
}
}
return UITableViewCell()
}
as you can see the expansionCells get a tag for my function plus and minus (so i can know which indexPath i should add up or down)
Plus function:
func plus(_ sender: AnyObject?) {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: sender!.tag)] {
self.table.reloadData()
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: sender!.tag)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = sender!.tag - parentCellIndex - 1
print(sender!.tag)
print(flightIndex)
print(rowData.menuItems!.count)
var con = 0;
for a in rowData.menuItems! {
print("her er navn : \(a.name) og her er id \(con)")
con += 1
}
let data = rowData.menuItems![flightIndex]
let item = Items(name: data.name, price: data.price, count: data.count)
var counter = 0;
for x in self.basket {
if(x.name == item.name || x.price == item.price)
{
counter = counter+1;
}
}
if(counter >= 99)
{
}
else
{
DispatchQueue.main.async {
self.basket.append(item)
self.updateBasket()
rowData.menuItems![flightIndex].count = rowData.menuItems![flightIndex].count+1;
self.table.reloadData()
}
}
}
}
Rest of the functions:
/* Expand cell at given index */
private func expandCell(tableView: UITableView, index: Int) {
// print("when is this run 1")
// Expand Cell (add ExpansionCells
if let flights = destinationData?[index]?.menuItems {
for i in 1...flights.count {
destinationData?.insert(nil, at: index + i)
tableView.insertRows(at: [NSIndexPath(row: index + i, section: 0) as IndexPath] , with: .top)
}
}
}
/* Contract cell at given index */
private func contractCell(tableView: UITableView, index: Int) {
// print("when is this run 4")
if let flights = destinationData?[index]?.menuItems {
for i in 1...flights.count {
destinationData?.remove(at: index+1)
tableView.deleteRows(at: [NSIndexPath(row: index+1, section: 0) as IndexPath], with: .top)
}
}
}
/* Get parent cell index for selected ExpansionCell */
private func getParentCellIndex(expansionIndex: Int) -> Int {
// print("when is this run 5")
var selectedCell: Menu?
var selectedCellIndex = expansionIndex
while(selectedCell == nil && selectedCellIndex >= 0) {
selectedCellIndex -= 1
selectedCell = destinationData?[selectedCellIndex]
}
return selectedCellIndex
}
func minus(_ sender: AnyObject?) {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: sender!.tag)] {
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: sender!.tag)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = sender!.tag - parentCellIndex - 1
let data = rowData.menuItems![flightIndex]
let item = Items(name: data.name, price: data.price, count: data.count)
DispatchQueue.main.async {
var counter = 0;
for x in self.basket {
if(x.name == item.name || x.price == item.price)
{
if(item.count == 0)
{
break
}
else
{
self.basket.remove(at: counter)
self.updateBasket()
rowData.menuItems![flightIndex].count = rowData.menuItems![flightIndex].count-1;
self.table.reloadData()
break
}
}
counter = counter+1;
}
}
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let data = destinationData?[indexPath.row] {
// If user clicked last cell, do not try to access cell+1 (out of range)
if(indexPath.row + 1 >= (destinationData?.count)!) {
expandCell(tableView: tableView, index: indexPath.row)
}
else {
// If next cell is not nil, then cell is not expanded
if(destinationData?[indexPath.row+1] != nil) {
expandCell(tableView: tableView, index: indexPath.row)
} else {
contractCell(tableView: tableView, index: indexPath.row)
}
}
}
}
I dont really know how to solve this, all i know is that the plus function gets out of range on this bit "let data = rowData.menuItems![flightIndex]".
How do I check if I have pressed on an NSSearchField search button?
search button of NSSearchField
#IBAction func searchTextField(_ sender: NSSearchField) {
if `searchButtonIsClicked` {
//Code if searchButtonIsClicked
return
}
if sender.stringValue != "" {
//My code
}
}
What I need to do instead of searchButtonIsClicked?
Here is the solution.
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var searchFieldText: NSSearchField!
#IBOutlet weak var labelSearchText: NSTextField!
#IBAction func searchFieldButton(_ sender: NSSearchField) {
if searchFieldText.stringValue != "" {
//My code. For example:
labelSearchText.stringValue = searchFieldText.stringValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let cellMenu = NSMenu(title: "Search Menu")
var item: NSMenuItem!
item = NSMenuItem(title: "Clear", action: nil, keyEquivalent: "")
item.tag = Int(NSSearchFieldClearRecentsMenuItemTag)
cellMenu.insertItem(item, at: 0)
item = NSMenuItem.separator()
item.tag = Int(NSSearchFieldRecentsTitleMenuItemTag)
cellMenu.insertItem(item, at: 1)
item = NSMenuItem(title: "Recent Searches", action: nil, keyEquivalent: "")
item.tag = Int(NSSearchFieldRecentsTitleMenuItemTag)
cellMenu.insertItem(item, at: 2)
item = NSMenuItem(title: "Recents", action: nil, keyEquivalent: "")
item.tag = Int(NSSearchFieldRecentsMenuItemTag)
cellMenu.insertItem(item, at: 3)
searchFieldText.searchMenuTemplate = cellMenu
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
Thank you for your help.
I think this demo can resolve your question.
Swift Code is below:
if let cell = searchField.cell as? NSSearchFieldCell {
let searchButtonCell: NSButtonCell = cell.searchButtonCell!
let cacelButtonCell: NSButtonCell = cell.cancelButtonCell!
searchButtonCell.target = self
cacelButtonCell.target = self
searchButtonCell.action = #selector(clickSearchButton(_:))
cacelButtonCell.action = #selector(clickCacelButton(_:))
}
DeleteBackward() deletes only one character, is there any way to keep on deleting backwards ?
I am using emojiKeyboard and I have a delete emoticon. I detect the emoji being the delete emoticon and I call
if emoticon.isDelete{
deleteBackward()
return
}
Update:
Steven's solution works on buttons but not on my UITextView. Will try and find out why. I have tried having the addGestureRecognizer in ViewWillAppear as well as ViewDidLoad.
This should get you started, didn't test but should do the trick.
fileprivate var timer = Timer()
fileprivate var textField = UITextField() //change to your field
override func viewDidLoad() {
super.viewDidLoad()
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
textField.addGestureRecognizer(longPress)
}
func longPress(_ guesture: UILongPressGestureRecognizer) {
if guesture.state == UIGestureRecognizerState.began {
longPressBegun(guesture)
} else if guesture.state == UIGestureRecognizerState.changed {
//longPressStateChanged(guesture)
} else if guesture.state == UIGestureRecognizerState.ended {
longPressEnded()
} else if guesture.state == UIGestureRecognizerState.cancelled {
longPressCancelled()
}
}
func longPressBegun(_ guesture: UILongPressGestureRecognizer) {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(repeatAction), userInfo: nil, repeats: true)
}
func longPressEnded() {
timer.invalidate()
}
func longPressCancelled() {
timer.invalidate()
}
func repeatAction() {
deleteBackward()
}