How to add a parameter to UITapGestureRecognizer so that the action function can access that parameter - ios

I have created UIViews programmatically based on the number of items i stored in my UserDefaults and each UIView represents an item from the userDefaults and have added UITapGestureRecognizer on top of it. Now this UIViews when clicked will send my user to a new view controller, now my problem is how do I pass a parameter which will hold a value so that the new view controller can determine which view was clicked. Below is my code
//Retrieving my userDefaults values
let items = preferences.object(forKey: selectedOffer)
//How i loop and create my UIViews
if let array = items as! NSArray?{
totalOffers = array.count
let castTotalOffers = CGFloat(totalOffers)
var topAnchorConstraint: CGFloat = 170
var cardHeight: CGFloat = 145
for obj in array {
if let dict = obj as? NSDictionary{
offerName = dict.value(forKey: "NAME") as! String
let offerPrice = dict.value(forKey: "PRICE") as! String
let offerDescription = dict.value(forKey: "DESCRIPTION") as! String
//creating the uiview
let offerView = UIView()
self.scrollView.addSubview(offerView)
offerView.translatesAutoresizingMaskIntoConstraints = false
offerView.topAnchor.constraint(equalTo: self.appBackImage.bottomAnchor, constant: topAnchorConstraint).isActive = true
offerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20.0).isActive = true
offerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20.0).isActive = true
offerView.backgroundColor = UIColor.white
offerView.heightAnchor.constraint(equalToConstant: 130).isActive = true
//transforming to cards
offerView.layer.cornerRadius = 2
offerView.layer.shadowOffset = CGSize(width: 0, height: 5)
offerView.layer.shadowColor = UIColor.black.cgColor
offerView.layer.shadowOpacity = 0.1
self.scrollView.contentSize.height = CGFloat(totalOffers) + topAnchorConstraint + 70
//Adding gesture
let touchRec = UITapGestureRecognizer(target: self, action: #selector(goToBuyBundle(offerClicked:offerName)))
offerView.addGestureRecognizer(touchRec)
}
}
}
//Function to go to buy offer
#objc func goToBuyBundle(_sender: UITapGestureRecognizer, offerClicked:String){
guard let moveTo = storyboard?.instantiateViewController(withIdentifier: "BuyOfferViewController") as? BuyOfferViewController else {return}
moveTo.selectedOffer = offerClicked
self.addChildViewController(moveTo)
moveTo.view.frame = self.view.frame
self.view.addSubview(moveTo.view)
moveTo.didMove(toParentViewController: self)
}
Just want a way when i navigate to the next view controller i can retrieve which UIView was clicked by using the offerName.
Thanks in Advance

Make your custom View and store the parameter that you want to pass through the Gesture Recognizer inside the view.
class GestureView: UIView{
var myViewValue: String? // Or whichever type of value you want to store and send via the gesture
}
When you initiate your view, add the value as per your requirement:
let panReceptor = GestureView()
panReceptor.myViewValue = "Hello World"
Add a simple TapGesture on this custom view and you may pass the value as below:
let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(viewTapped(sender:)))
panReceptor.addGestureRecognizer(tapGesture)
#objc func viewTapped(sender: UITapGestureRecognizer){
guard let unwrappedView = sender.view as? GestureView else { return }
print("Gesture View value : \(unwrappedView.myViewValue)")
}
In the above example I have in effect passed a String parameter through the sender.view.
You may pass any type in this manner and use the value as per your requirement in the selector method.

You could add custom variable to UITapGestureRecognizer something like:
import UIKit
private var assocKey : UInt8 = 0
extension UITapGestureRecognizer {
public var offerName:String{
get{
return objc_getAssociatedObject(self, &assocKey) as! String
}
set(newValue){
objc_setAssociatedObject(self, &assocKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}
And then use it like:
...
let touchRec = UITapGestureRecognizer(target: self, action: #selector(goToBuyBundle(offerClicked:offerName)))
touchRec.offerName = offerName
offerView.addGestureRecognizer(touchRec)
...
#objc func goToBuyBundle(_sender: UITapGestureRecognizer, offerClicked:String){
guard let moveTo = storyboard?.instantiateViewController(withIdentifier: "BuyOfferViewController") as? BuyOfferViewController else {return}
moveTo.selectedOffer = sender.offerName
...
}

Related

How To Add [int:int] element in user Defaults in swift5

I used to many attempts to solve this error but i can't.
i have [Int: Int] dictionary
want's to add this dictionary to default for use latter
Sample Code Of mine
var ListIds = [Int: Int]()
ListIds[1] = 1
defaults.set(["data": ListIds], forKey: "cartKeys")
Some quick web searching and I found this: Hacking with Swift forums - Save [Int:Int] to UserDefaults
I edited the code slightly to make it an extension, and to allow passing the Key to use in UserDefaults:
extension UserDefaults {
func saveIntDictionary(key: String, intDictionary: [Int:Int]) {
let encoder = PropertyListEncoder()
guard let data = try? encoder.encode(intDictionary) else {
return
}
set(data, forKey: key)
}
func retrieveSavedIntDictionary(key: String) -> [Int:Int]? {
let decoder = PropertyListDecoder()
guard let data = data(forKey: key),
let intDictionary = try? decoder.decode([Int:Int].self, from: data) else {
return nil
}
return intDictionary
}
}
and it can be used like this:
// note: it returns an optional -- it will be nil if the key does not exist
// handle that appropriately
let sv = UserDefaults.standard.retrieveSavedIntDictionary(key: "cartKeys")
// save to UserDefaults
UserDefaults.standard.saveIntDictionary(key: "cartKeys", intDictionary: listIDs)
Here's a quick runnable example:
class ViewController: UIViewController {
var listIDs: [Int : Int] = [:]
// a scrollable non-editable text view to display the dictionary
let displayTextView = UITextView()
override func viewDidLoad() {
super.viewDidLoad()
if let sv = UserDefaults.standard.retrieveSavedIntDictionary(key: "cartKeys") {
listIDs = sv
print("Loaded from UserDefaults!")
print(String(describing: listIDs))
print()
} else {
print("Nothing in UserDefaults")
print()
}
// let's have two buttons
// Add a new ID
// Reset (clears the dictionary)
let b1 = UIButton()
b1.setTitle("Add a new ID", for: [])
b1.setTitleColor(.white, for: .normal)
b1.setTitleColor(.lightGray, for: .highlighted)
b1.backgroundColor = .systemBlue
b1.layer.cornerRadius = 8
b1.addTarget(self, action: #selector(addEntry(_:)), for: .touchUpInside)
let b2 = UIButton()
b2.setTitle("Reset", for: [])
b2.setTitleColor(.white, for: .normal)
b2.setTitleColor(.lightGray, for: .highlighted)
b2.backgroundColor = .systemRed
b2.layer.cornerRadius = 8
b2.addTarget(self, action: #selector(reset(_:)), for: .touchUpInside)
displayTextView.backgroundColor = .yellow
displayTextView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
displayTextView.isEditable = false
// a stack view for the buttons and display text view
let stack = UIStackView(arrangedSubviews: [b1, displayTextView, b2])
stack.axis = .vertical
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
stack.centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: -20.0),
displayTextView.heightAnchor.constraint(equalToConstant: 240.0),
])
updateDisplay()
}
func updateDisplay() {
var s: String = ""
if listIDs.isEmpty {
s = "No entries in listIDs"
} else {
listIDs.forEach { item in
s += "Key: \(item.key)\t\tValue: \(item.value)\n"
}
}
displayTextView.text = s
}
#objc func addEntry(_ sender: Any?) {
// add a new entry
let n: Int = listIDs.count + 1
let v: Int = Int.random(in: 1...20000)
listIDs[n] = v
// save to UserDefaults
UserDefaults.standard.saveIntDictionary(key: "cartKeys", intDictionary: listIDs)
updateDisplay()
}
#objc func reset(_ sender: Any?) {
// clear the dictionary
listIDs = [:]
// save to UserDefaults
UserDefaults.standard.removeObject(forKey: "cartKeys")
//UserDefaults.standard.saveIntDictionary(key: "cartKeysA", intDictionary: listIDs)
updateDisplay()
}
}
When run, it will look like this. Each tap of "Add a new ID" will add a new sequential entry to the dictionary (with a random value) and save to UserDefaults. Quit the app and re-run it to see the dictionary loaded. Tap "Reset" to clear it:
A couple notes:
1 - Learn about naming conventions... listIDs instead of ListIds for example.
2 - Saving "data" this way is generally not a good idea -- UserDefaults is much better suited to storing things like "app settings" for example. If your dictionary of "IDs" may grow large, you probably want to look at other data persistence methods.

Primitive type parameters in Swift Selector function

I want to add dynamic number of buttons to my VC. So i am looping through my buttons array model and instantiating UIButtons. The problem is with adding target to these buttons. I want to pass in a string to the selector when adding a target, however Xcode compiler doesn't let me do that
Argument of '#selector' does not refer to an '#objc' method, property, or initializer
#objc func didTapOnButton(url: String) { }
let button = UIButton()
button.addTarget(self, action: #selector(didTapOnButton(url: "Random string which is different for every bbutton ")), for: .touchUpInside)
Is there any other solution other than using a custom UIButton
I don't think it is possible to do what you are attempting, you can try like this:
var buttons: [UIButton: String] = []
let button = UIButton()
let urlString = "Random string which is different for every button"
buttons[button] = urlString
button.addTarget(self, action: #selector(didTapOnButton), for: .touchUpInside
#objc func didTapOnButton(sender: UIButton) {
let urlString = self.buttons[sender]
// Do something with my URL
}
As I remember UIButton is hashable...
Another option would be to extend UIButton to hold the information you want:
extension UIButton {
private static var _urlStringComputedProperty = [String: String]()
var urlString String {
get {
let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
return Self._urlStringComputedProperty[tmpAddress]
}
set(newValue) {
let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
Self._urlStringComputedProperty[tmpAddress] = newValue
}
}
}
let button = UIButton()
button.urlString = "Random string which is different for every button"
button.addTarget(self, action: #selector(didTapOnButton), for: .touchUpInside
#objc func didTapOnButton(sender: UIButton) {
let urlString = sender.urlString
// Do something with my URL
}

Activity Indicator for UICollectionView [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 5 years ago.
Improve this question
I have a UICollectionView that reads data from Firebase, and I want some kind of activity indicator while it's reading data.
I want the activity indicator to start running as soon as the UICollectionView tab in the TabBar is hit, and I want it to stop and the view/uicollectionView to load once loading from Firebase is done.
I saw this post:
Show Activity Indicator while data load in collectionView Swift
But I could not understand it fully because I did not know how to integrate my UICollectionView there.
EDIT:
This is my code that reads from Firebase:
self.ref = Database.database().reference()
let loggedOnUserID = Auth.auth().currentUser?.uid
if let currentUserID = loggedOnUserID
{
// Retrieve the products and listen for changes
self.databaseHandle = self.ref?.child("Users").child(currentUserID).child("Products").observe(.childAdded, with:
{ (snapshot) in
// Code to execute when new product is added
let prodValue = snapshot.value as? NSDictionary
let prodName = prodValue?["Name"] as? String ?? ""
let prodPrice = prodValue?["Price"] as? Double ?? -1
let prodDesc = prodValue?["Description"] as? String ?? ""
let prodURLS = prodValue?["MainImage"] as? String
let prodAmount = prodValue?["Amount"] as? Int ?? 0
let prodID = snapshot.key
let prodToAddToView = Product(name: prodName, price: prodPrice, currency: "NIS", description: prodDesc, location: "IL",
toSell: false, toBuy: false, owner: currentUserID, uniqueID: prodID, amount: prodAmount, mainImageURL: prodURLS)
self.products.append(prodToAddToView)
DispatchQueue.main.async
{
self.MyProductsCollection.reloadData()
}
}
) // Closes observe function
let activityView = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
// waiy until main view shows
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// create a hover view that covers all screen with opacity 0.4 to show a waiting action
let fadeView:UIView = UIView()
fadeView.frame = self.view.frame
fadeView.backgroundColor = UIColor.whiteColor()
fadeView.alpha = 0.4
// add fade view to main view
self.view.addSubview(fadeView)
// add activity to main view
self.view.addSubview(activityView)
activityView.hidesWhenStopped = true
activityView.center = self.view.center
// start animating activity view
activityView.startAnimating()
self.ref = Database.database().reference()
let loggedOnUserID = Auth.auth().currentUser?.uid
if let currentUserID = loggedOnUserID
{
// Retrieve the products and listen for changes
self.databaseHandle = self.ref?.child("Users").child(currentUserID).child("Products").observeSingleEvent(.value, with:
{ (snapshot) in
// Code to execute when new product is added
let prodValue = snapshot.value as? NSDictionary
let prodName = prodValue?["Name"] as? String ?? ""
let prodPrice = prodValue?["Price"] as? Double ?? -1
let prodDesc = prodValue?["Description"] as? String ?? ""
let prodURLS = prodValue?["MainImage"] as? String
let prodAmount = prodValue?["Amount"] as? Int ?? 0
let prodID = snapshot.key
let prodToAddToView = Product(name: prodName, price: prodPrice, currency: "NIS", description: prodDesc, location: "IL",
toSell: false, toBuy: false, owner: currentUserID, uniqueID: prodID, amount: prodAmount, mainImageURL: prodURLS)
self.products.append(prodToAddToView)
DispatchQueue.main.async
{
self.MyProductsCollection.reloadData()
// remove the hover view as now we have data
fadeView.removeFromSuperview()
// stop animating the activity
self.activityView.stopAnimating()
}
}
) // Closes observe function
I mean UIActivityIndicatorView isn't hard to work with, you display it when fetching data, and stop it when done.
private lazy var indicator : UIActivityIndicatorView = { // here we simply declaring the indicator along some properties
let _indicator = UIActivityIndicatorView()
// change the color
_indicator.color = .black
// when you call stopAnimation on this indicator, it will hide automatically
_indicator.hidesWhenStopped = true
return _indicator
}()
Now where you want to place it? you can either placed it into your parent's view, or into your navigationBar. (I choose to place into the right side of the navigationBar )
self.navigationItem.rightBarButtonItem = UIBarButtonItem.init(customView: indicator)
Now say you have this function that return data (via callbacks) from some apis.
// this callback emits data from a background queue
func fetchPost(completion:#escaping(Array<Any>?, Error?) -> ()) {
DispatchQueue.global(qos: .background).async {
// ... do work
completion([], nil) // call your completionHandler based either error or data
}
}
/* now let's called that fetchPost function and load data into your
collectionView, but before let's started this indicator */
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem.init(customView: indicator)
indicator.startAnimating()
fetchPost { [weak self] (data, err) in
// go to the main queue to update our collectionView
// and stop the indicator
DispatchQueue.main.async { [weak self] in
// stop animation
self?.indicator.startAnimating()
// do something we have an error
if let _err = err {}
if let _data = data {
// fill array for collectionView
// reload the collectionView
self?.collectionView?.reloadData()
}
}
}
}

Getting a nil in Swift 3?

I am creating an app where I have annotation view showing that when you click it shows on the DetailsViewController that annotation data. However, I get "Name", and "Address" data but for phone Number I am getting set as nil. So, if you guys can see my code & help me solve it, I will be appreciated it.
Here is my code:
import UIKit
import MapKit
protocol UserLocationDelegate {
func userLocation(latitude :Double, longitude :Double)
}
class NearMeMapViewController: ARViewController, ARDataSource, MKMapViewDelegate, CLLocationManagerDelegate {
var nearMeIndexSelected = NearMeIndexTitle ()
var locationManager : CLLocationManager!
var nearMeARAnnotations = [ARAnnotation]()
var nearMeRequests = [NearMeRequest]()
var delegate : UserLocationDelegate!
var place: Place?
override func viewDidLoad() {
super.viewDidLoad()
self.title = nearMeIndexSelected.indexTitle
self.locationManager = CLLocationManager ()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.distanceFilter = kCLHeadingFilterNone
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
self.dataSource = self
self.headingSmoothingFactor = 0.05
self.maxVisibleAnnotations = 30
getNearMeIndexSelectedLocation()
}
func getNearMeIndexSelectedLocation()
{
let nearMeRequest = MKLocalSearchRequest()
nearMeRequest.naturalLanguageQuery = nearMeIndexSelected.indexTitle
let nearMeregion = MKCoordinateRegionMakeWithDistance(self.locationManager.location!.coordinate, 250, 250)
nearMeRequest.region = nearMeregion
let nearMeSearch = MKLocalSearch(request: nearMeRequest)
nearMeSearch.start { (response : MKLocalSearchResponse?, error :Error?) in
for requestItem in (response?.mapItems)! {
let nearMeIndexRequest = NearMeRequest ()
nearMeIndexRequest.name = requestItem.name
nearMeIndexRequest.coordinate = requestItem.placemark.coordinate
nearMeIndexRequest.address = requestItem.placemark.addressDictionary?["FormattedAddressLines"] as! [String]
nearMeIndexRequest.street = requestItem.placemark.addressDictionary?["Street"] as! String!
nearMeIndexRequest.city = requestItem.placemark.addressDictionary?["City"] as! String
nearMeIndexRequest.state = requestItem.placemark.addressDictionary?["State"] as! String
nearMeIndexRequest.zip = requestItem.placemark.addressDictionary?["ZIP"] as! String
self.nearMeRequests.append(nearMeIndexRequest)
print(requestItem.placemark.name)
}
for nearMe in self.nearMeRequests {
let annotation = NearMeAnnotation(nearMeRequest: nearMe)
self.nearMeARAnnotations.append(annotation)
self.setAnnotations(self.nearMeARAnnotations)
}
}
}
func ar(_ arViewController: ARViewController, viewForAnnotation: ARAnnotation) -> ARAnnotationView {
let annotationView = NearMeARAnnotationView(annotation: viewForAnnotation)
// annotationView.delegate = self
annotationView.frame = CGRect(x: 0, y: 0, width: 150, height: 50)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.tapBlurButton(_:)))
annotationView.addGestureRecognizer(tapGesture)
return annotationView
}
func tapBlurButton(_ sender: UITapGestureRecognizer) {
if let annotationView = sender.view as? NearMeARAnnotationView {
if let detailsVc = storyboard?.instantiateViewController(withIdentifier: "DetailsViewController")
as? DetailsViewController {
detailsVc.annotation = annotationView.annotation
if let annotation = annotationView.annotation as? Place {
detailsVc.place = annotation
}
self.navigationController?.pushViewController(detailsVc, animated: true)
}
}
}
}
Just looking over your code quickly:
"\(nearMeAnnotation.nearMeRequest.phone)"
All the other ones have a forced unwrap, this one doesn't. Most probably the value is nil and since you ask for a string representation of a wrapped var that might really be nil sometimes.
I think you should use a default value everywhere instead of a forced unwrap, like:
"\(nearMeAnnotation.nearMeRequest.phone ?? "")"
but also:
"\(nearMeAnnotation.nearMeRequest.street ?? "") \(nearMeAnnotation.nearMeRequest.state ?? "") \(nearMeAnnotation.nearMeRequest.state ?? "") \(nearMeAnnotation.nearMeRequest.zip ?? "")"
With forced unwraps your application will crash if a certain value is not set. This could be handled more elegantly if they're really required, for example already in the constructor of your object. There's the root cause of the optionals you're seeing. In this case NearMeAnnotation.

UITextView open link on tap

I use this code:
var textView = UITextView(x: 10, y: 10, width: CardWidth - 20, height: placeholderHeight) //This is my custom initializer
textView.text = "dsfadsaf www.google.com"
textView.selectable = true
textView.dataDetectorTypes = UIDataDetectorTypes.Link
textView.delegate = self
addSubview(textView)
The problem is the link needs long tap gesture to open. I want it to open with a single tap, just like in the Facebook app.
The example below only works on iOS 8+
Tap recognizer:
let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("tappedTextView:"))
myTextView.addGestureRecognizer(tapRecognizer)
myTextView.selectable = true
Callback:
func tappedTextView(tapGesture: UIGestureRecognizer) {
let textView = tapGesture.view as! UITextView
let tapLocation = tapGesture.locationInView(textView)
let textPosition = textView.closestPositionToPoint(tapLocation)
let attr: NSDictionary = textView.textStylingAtPosition(textPosition, inDirection: UITextStorageDirection.Forward)
if let url: NSURL = attr[NSLinkAttributeName] as? NSURL {
UIApplication.sharedApplication().openURL(url)
}
}
Swift 3 and no force unwraps:
func tappedTextView(tapGesture: UIGestureRecognizer) {
guard let textView = tapGesture.view as? UITextView else { return }
guard let position = textView.closestPosition(to: tapGesture.location(in: textView)) else { return }
if let url = textView.textStyling(at: position, in: .forward)?[NSLinkAttributeName] as? URL {
UIApplication.shared.open(url)
}
}

Resources