Is it possible to display a MKClusterAnnotation callout? - ios11

Note: I am using iOS11s native mapview annotation clustering.
In a situation where annotations are still clustered at max zoom, in what manner can we show a callout?
I'm showing a pop-over type view to display a list of annotations at the cluster, but calling selectAnnotation isn't enough to show a callout for an annotation that is "clustered".
"Something" is selected, but no callout is shown. By something, I just mean that my didDeselect view method is called after I touch the mapview.

I ran through the same problem. Seems that they didn't think carefully in that case. You must select the MKClusterAnnotation instead the MKAnnotation that is clustered but it's not simple to get there.
on iOS11 there's a property on MKAnnotationView called cluster that as the documentation states is:
If non-nil this is the annotation view this view is clustered into.
So in my MKAnnotationView subclass I override the setSelected method and with a weak reference to the mapView you must select the cluster one:
//You have weak reference to mapView
weak var mapView: MKMapView?
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if #available(iOS 11.0, *) {
if selected, let cluster = cluster {
// Deselect the current annotation (Maybe this step is not required, didn't check it)
if let annotation = annotation {
mapView?.deselectAnnotation(annotation, animated: false)
}
// Select the cluster annotation
if let clusterAnnotation = cluster.annotation {
mapView?.selectAnnotation(clusterAnnotation, animated: true)
}
}
}
}

Its actually quite simple. The map view doesn't bother showing a callout if the assigned MKMarkerAnnotationView is not set to show callouts through .canShowCallout and also if there are no accessories on the view (that's the important one). If those two conditions are not met then the map can show the title and subtitle on the pin itself. So, here is all you have to do:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?
{
if annotation is MyAnnotationConformingClass {
let annotation = annotation as! MKAnnotation
let view = MKAannotationView(annotation: annotation, reuseIdentifier: "pinReUserId")
view.canShowCallout = true
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
return view
}
if annotation is MKClusterAnnotation {
let annotation = annotation as! MKClusterAnnotation
let view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "ClusterResuseId")
view.canShowCallout = true
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
return view
}
return nil
}
By giving the cluster's MKMarkerAnnotationView an accessory and also allowing show callouts, the callout will then be shown. If you remember with older SDKs, the map would not show a callout if you had no title and subtitle set.

Related

PHPicker delegate error: PHPickerViewControllerDelegate doesn't respond to picker:didFinishPicking

In WWDC20 apple introduced PHPicker, the replacement for UIImagePickerController.
I have a wrapper NSObject class witch handles all the configuration and presentation of image picker controller, now I'm replacing the implementation to support iOS14.
Even if I set the delegate to be self I get the error:
[Picker] PHPickerViewControllerDelegate doesn't respond to picker:didFinishPicking:
I think it checks on the parent view controller, that indeed it's not implementing the delegate methods but the wrapper does.
Here is my example code:
import Foundation
import PhotosUI
class myPicker: PHPickerViewControllerDelegate{
func openFrom(parent:UIViewController!) {
var config:PHPickerConfiguration! = PHPickerConfiguration()
config.selectionLimit = 1
config.filter = nil
let pickerViewController:PHPickerViewController! = PHPickerViewController(configuration:config)
pickerViewController.delegate = self //<---
parent.present(pickerViewController, animated:true, completion:nil)
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true, completion:nil)
for result:PHPickerResult in results {
let itemProvider:NSItemProvider = result.itemProvider
print(itemProvider)
}
// ...do something with images...
}
}
Using it...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let mypicker = myPicker()
mypicker.openFrom(parent: self)
}
What do you suggest?
The problem is that Cocoa Objective-C cannot introspect your class. Change
class myPicker: PHPickerViewControllerDelegate
To
class myPicker: NSObject, PHPickerViewControllerDelegate
You need to make "class myPicker" as a singleton class. Because you should make sure delegate not to be nil. Also, you can change this instance = nil in the didFinishPicking method.

iOS13.1 UIAlertController with preferedStyle: .actionSheet cannot change title's text color and font

I change title font and color like this:
let titleAttributes = [NSAttributedStringKey.font: UIFont(name: "HelveticaNeue-Bold", size: 25)!, NSAttributedStringKey.foregroundColor: UIColor.purple]
alert.setValue(titleString, forKey: "attributedTitle")
Before iOS13 this worked fine both for preferredStyle .alert and .actionSheet.
Now it only works for .alert and doesn't work for .actionSheet.
Someone please any help?
iOS 13 now embeds the UIAlertController title in a UIVisualEffectView, and only the title's alpha channel affects its appearance. If it is critical to control the exact color, I think you could try finding the subview class _UIInterfaceActionGroupHeaderScrollView, remove its child UIVisualEffectView, and then add your own UILabel back in. But no guarantees it will work or that it won't trigger a crash. You can use the allSubviews extension below and compare each to this:
let grpHeader = NSClassFromString("_UIInterfaceActionGroupHeaderScrollView")
Or if you just need to ensure that your title is visible based on your app's color scheme I had success with the following, which should be quite safe to use at least until iOS 14 is released.
let alertController = UIAlertController(....)
for subView in UIView.allSubviews(of: alertController.view) {
if let effectView = subView as? UIVisualEffectView {
if effectView.effect is UIVibrancyEffect {
if #available(iOS 13.0, *) {
// iOS 13.1 default blur style is UIBlurEffectStyleSystemVibrantBackgroundRegular which is NOT currently defined anywhere
// if your alert controller color is dark, set to .systemMaterialDark
// if your alert controller color is light, set to .systemMaterialLight
effectView.effect = UIVibrancyEffect(blurEffect: UIBlurEffect(style: UIBlurEffect.Style.systemMaterialDark), style: .secondaryLabel)
}
break
}
}
}
// You will need this UIView category to get an array of all subviews,
// as the view heirarchy is complex. In iOS13 it is:
// UIAlertController.UIView ->
// _UIAlertControllerInterfaceActionGroupView ->
// UIView
// _UIInterfaceActionGroupHeaderScrollView
// ** UIVisualEffectView **
// _UIInterfaceActionRepresentationsSequenceView
// _UIDimmingKnockoutBackdropView ]
// ** all your button actions **
extension UIView {
class func allSubviews<T : UIView>(of view: UIView) -> [T] {
var subviews = [T]()
for subview in view.subviews {
subviews += allSubviews(of: subview) as [T]
if let subview = subview as? T {
subviews.append(subview)
}
}
return subviews
}
}

ARKit – Add a "SCNNode" to an "ARAnchor"

I'm not sure I am approaching this correctly. I have a long rectangular box that I want to add -1.5 from the camera when the app starts up. But I want it to be stationary, like the ship that comes default in an ARKit project. But whenever I add it, the object stays relative (distance wise) to the camera. i.e - move towards it, it moves back, move back, it moves forward.
I though dropping an anchor on the scene would resolve this but I am still getting the same affect. Here is my code. Any help would be appreciated:
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
//sceneView.showsStatistics = true
// Create a new scene
let scene = SCNScene()//
// Set the scene to the view
sceneView.scene = scene
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
//configuration.planeDetection = .horizontal
// Run the view's session
sceneView.session.run(configuration)
print(#function, sceneView.session.currentFrame)
}
// MARK: - SCNSceneRendererDelegate
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
print(#function, sceneView.session.currentFrame)
if !hasPortalAnchor {
//add anchor - this may take a second as the current frames are initially nil
if let currentFrame = sceneView.session.currentFrame {
var translation = matrix_identity_float4x4
translation.columns.3.z = -1.3
let transform = simd_mul(currentFrame.camera.transform, translation)
if (arrAnchors.count < 1) {
let portalAnchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: portalAnchor)
arrAnchors.append(portalAnchor)
print(arrAnchors)
}
}
} else {
hasPortalAnchor = true
}
}
//this function gets called whenever we add an anchor to our scene
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
let portalScene = SCNScene(named: "art.scnassets/portal.scn")!
return portalScene.rootNode.childNode(withName: "portal", recursively: true)
}
Are you deliberately using the renderer(_:) function? If not then you can just use the following viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
let scene = SCNScene(named: "art.scnassets/portal.scn")!
sceneView.scene = scene
}
This will replace the default rocket that appears on startup, with your portal scene. (Note that the scene may move around if tracking hasn't been established. For instance if the light is low, or if you are in an environment without many features (a blank or repetitive wall for instance)).
Also it looks like you're not actually setting hasPortalAnchor to true? Is it being set somewhere else?

TableView search in Swift

I have two arrays: FirstTableArray (include name of brands) and SecondTableArray (include models).
I want to add search through which the model of phone can be found by part of name.
import UIKit
import MessageUI
class FirstTableViewController: UITableViewController {
var FirstTableArray = [String]()
var SecondTableArray = [SecondTable]()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.barStyle = .black
self.navigationController?.navigationBar.tintColor = UIColor.white
// First Table Array
FirstTableArray = ["Apple", "Samsung"]
// Second Table Array
SecondTableArray = [
SecondTable(SecondTitle: ["iPhone 5s", "iPhone 6", "iPhone 6s"]),
SecondTable(SecondTitle: ["Galaxy S4", "Galaxy S5", "Galaxy S6"]),
]
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return FirstTableArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let Cell = self.tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as UITableViewCell
Cell.textLabel?.text = FirstTableArray[(indexPath as NSIndexPath).row]
return Cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let indexPath : IndexPath = self.tableView.indexPathForSelectedRow!
let DestViewController = segue.destination as! SecondTableViewController
let SecondTableArrayTwo = SecondTableArray[(indexPath as NSIndexPath).row]
DestViewController.SecondTableArray = SecondTableArrayTwo.SecondTitle
}
}
Can you help me with this?
I'm working through the same thing today and found this tutorial very easy to follow: https://github.com/codepath/ios_guides/wiki/Search-Bar-Guide
It will take you through the steps of adding the search bar in Interface Builder, setting up the delegate, and including a method to filter the results.
Providing a way for users to search through a collection of items is a fairly common task in iOS projects. A standard interface for implementing search behaviors is the search bar.
There are a few common ways to work with Search Bars:
Directly using a UISearchBar. This is the most bare bones way to use
UISearchBars. This can be extremely flexible if you want to design
and program your own search interface, however does not provide as
many built-in features as the other methods.
Using a UISearchDisplayController to help manage a search interface.
The UISearchDisplayController allows you to present a standard search
interface with built-in animations. This method forces you to display
search results in a table view. - DEPRECATED
Using a UISearchController to help manage a search interface. The
UISearchController is a newer controller (available only in iOS 8+)
that helps you present a search interface using any kind of view to
display the search results.
This guide covers the very basics of working with each of these classes. None of these classes actually implements the "searching" behavior of finding items that match a given query string, since determining which objects match will vary with the domain specific use case (e.g. when searching for "people" you might want to match on just their names, whereas you may want a full-text pre-indexed search when searching through e-mails). You'll have to implement any search/filtering behavior yourself.
Working with UISearchBars directly
At its core, a search bar is nothing more than a glorified text field packaged with a scope control and some animations and a couple of buttons. Each search bar has a delegate that gives you an opportunity to respond to user actions. The most important delegate methods are:
textDidChange - most of the time you'll respond to this event by
updating the displayed set of search results as the user is typing
out a query
searchBarSearchButtonClicked - in some cases if the search operation
is slow (e.g. requires making a slow API call) you'll want to wait
until the user taps the search button before updating the search
results.
Example searching a table
We start out with a single view application with a basic UITableView. You can add a UISearchBar as you would with any other control by dragging one to your view controller in interface builder or by programmatically adding it.
The delegate property of search bar must be set to an object that implements UISearchBarDelegate. Typically you make your view controller implement UISearchBarDelegate and set searchBar.delegate = self in viewDidLoad method.
The code to implement the search behavior is as follows. We maintain an additional array filteredData to represent rows of data that match our search text. When the search text changes we update filteredData and reload our table.
class ViewController: UIViewController, UITableViewDataSource, UISearchBarDelegate {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var searchBar: UISearchBar!
let data = ["New York, NY", "Los Angeles, CA", "Chicago, IL", "Houston, TX",
"Philadelphia, PA", "Phoenix, AZ", "San Diego, CA", "San Antonio, TX",
"Dallas, TX", "Detroit, MI", "San Jose, CA", "Indianapolis, IN",
"Jacksonville, FL", "San Francisco, CA", "Columbus, OH", "Austin, TX",
"Memphis, TN", "Baltimore, MD", "Charlotte, ND", "Fort Worth, TX"]
var filteredData: [String]!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
searchBar.delegate = self
filteredData = data
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableCell", for: indexPath) as UITableViewCell
cell.textLabel?.text = filteredData[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredData.count
}
// This method updates filteredData based on the text in the Search Box
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// When there is no text, filteredData is the same as the original data
// When user has entered text into the search box
// Use the filter method to iterate over all items in the data array
// For each item, return true if the item should be included and false if the
// item should NOT be included
filteredData = searchText.isEmpty ? data : data.filter({(dataString: String) -> Bool in
// If dataItem matches the searchText, return true to include it
return dataString.range(of: searchText, options: .caseInsensitive) != nil
})
tableView.reloadData()
}
}
Here's what this looks like when running. Notice that the search results are displayed in the same table, and there is no presentation of a separate search interface.
Example searching a collection view
Since the UISearchBar is quite simple, it can be combined with any abitrary view to build your own search interface. Here's what it might look like paired with a collection view.
The code for this is essentially the same as in the case with table views.
Cancelling out of Search and hiding keyboard
Once user taps on search bar, the keyboard will appear, and you will notice that it won't go away when you tap on X. You can show Cancel button when user taps on search bar, and when user taps on Cancel, hide the keyboard.
There is a nifty searchBarTextDidBeginEditing method for UISearchBarDelegate that gets called when user starts editing search text. You can show Cancel button in that method:
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
self.searchBar.showsCancelButton = true
}
When user taps on cancel button, delegate's searchBarCancelButtonClicked method gets called. At this point, you can hide the Cancel button, clear existing text in search bar and hide the keyboard like this:
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = false
searchBar.text = ""
searchBar.resignFirstResponder()
}
Using UISearchControllers (iOS 8+)
A newer way to manage the presentation of a search interface (only available in iOS 8 and above) is via the UISearchController. This controller handles some of the logic and animation of presenting a separate search interface for you while still allowing you to specify how your search results are displayed.
Example searching a table
There is currently no built-in object in the Interface Builder Object Library for a UISearchController. The easiest way to create one is to do it programatically. This also creates a UISearchBar and sets the search controller's searchBar property to it. You can add this search bar to your view hierarchy programatically.
In order to update your search results you'll have to implement the UISearchResultsUpdating protocol and set the search controller's searchResultsUpdater property.
You don't need to implement the UISearchControllerDelegate unless you need to hook into the events around the presentation of the search interface.
Putting it all together the code looks like this. Notice that we have to read the search text from the search bar in updateSearchResultsForSearchController. One other thing to note is that we set this view controller's definesPresentationContext property to true. This means that the search controller should use this view controller's frame (as oppposed to the root view controller) when presenting the search interface. In this case it means that the search interface will expand above the carrier bar.
class ViewController: UIViewController, UITableViewDataSource, UISearchResultsUpdating {
#IBOutlet weak var tableView: UITableView!
let data = ["New York, NY", "Los Angeles, CA", "Chicago, IL", "Houston, TX",
"Philadelphia, PA", "Phoenix, AZ", "San Diego, CA", "San Antonio, TX",
"Dallas, TX", "Detroit, MI", "San Jose, CA", "Indianapolis, IN",
"Jacksonville, FL", "San Francisco, CA", "Columbus, OH", "Austin, TX",
"Memphis, TN", "Baltimore, MD", "Charlotte, ND", "Fort Worth, TX"]
var filteredData: [String]!
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
filteredData = data
// Initializing with searchResultsController set to nil means that
// searchController will use this view controller to display the search results
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
// If we are using this same view controller to present the results
// dimming it out wouldn't make sense. Should probably only set
// this to yes if using another controller to display the search results.
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.sizeToFit()
tableView.tableHeaderView = searchController.searchBar
// Sets this view controller as presenting view controller for the search interface
definesPresentationContext = true
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("TableCell") as UITableViewCell
cell.textLabel?.text = filteredData[indexPath.row]
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredData.count
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
if let searchText = searchController.searchBar.text {
filteredData = searchText.isEmpty ? data : data.filter({(dataString: String) -> Bool in
return dataString.rangeOfString(searchText, options: .CaseInsensitiveSearch) != nil
})
tableView.reloadData()
}
}
}
Here's what this looks like when running. Notice that unlike in the search display controller example, we are using the same table view to display the search results instead of overlaying of a separate table view. However, unlike when working with just the search bar, we still have the built in animation when transitioning to the search interface.
Also, you get the logic to show Cancel button and hide keyboard when user taps on cancel button for free when you use this.
Example searching a collection view
We can just as easily use the search controller to search a collection view in place. We still have the presentation of a search interface, but unlike when working with the search display controller we are not restricted to using a table view to display the search results.
The code for this is almost the same as when searching the the table view above. The only notable difference is that we had to introduce a placeholder view in interface builder for the search bar since there are still some quirks with placing a search controller's search bar inside a collection view's supplementary view.
class ViewController: UIViewController, UICollectionViewDataSource, UISearchResultsUpdating {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var searchBarPlaceholder: UIView!
...
override func viewDidLoad() {
...
searchController.searchBar.sizeToFit()
searchBarPlaceholder.addSubview(searchController.searchBar)
automaticallyAdjustsScrollViewInsets = false
definesPresentationContext = true
}
...
}
Search Bar in Navigation View
A common requirement is to place the search bar inside the navigation bar.
This can be configured programatically in your view controller's viewDidLoad as follows.
When working directly with a search bar:
// create the search bar programatically since you won't be
// able to drag one onto the navigation bar
searchBar = UISearchBar()
searchBar.sizeToFit()
// the UIViewController comes with a navigationItem property
// this will automatically be initialized for you if when the
// view controller is added to a navigation controller's stack
// you just need to set the titleView to be the search bar
navigationItem.titleView = searchBar
Using a search display controller:
searchDisplayController?.displaysSearchBarInNavigationBar = true
Using a search controller:
searchController.searchBar.sizeToFit()
navigationItem.titleView = searchController.searchBar
// By default the navigation bar hides when presenting the
// search interface. Obviously we don't want this to happen if
// our search bar is inside the navigation bar.
searchController.hidesNavigationBarDuringPresentation = false
I would suggest implemeting a UISearchBar and adding your class as a delegate to the UISearchBar and then in the delegates methods you can take the searchbar text and perform the search on the datasouce and then reload the tableview data accordingly
Edit: You can use this tutorial for how to implement UISearchBar on a UITableView
https://www.raywenderlich.com/472-uisearchcontroller-tutorial-getting-started

UIPageViewController disable scrolling

I am using a UIPageViewController with transitionStyle UIPageViewControllerTransitionStyleScroll and navigationOrientation UIPageViewControllerNavigationOrientationVertical
I also have a UIPanGestureRecognizer on the view and I want to disable page scrolling when the pan gesture is active.
I am trying to set the following when the gesture begins:
pageViewController.view.userInteractionEnabled = NO;
This seems to have no effect, or it appears to work sporadically.
The only other way I have found to do it (which works) is to set the UIPageViewController dataSource to nil while the pan gesture is running, however this causes a huge delay when resetting the dataSource.
UIPageViewController uses some UIScrollView object to handle scrolling (at least for transitionStyle UIPageViewControllerTransitionStyleScroll). You can iterate by controller's subviews pageViewController.view.subviews to get it. Now, you can easly enable/disable scrolling:
- (void)setScrollEnabled:(BOOL)enabled forPageViewController:(UIPageViewController*)pageViewController
{
for (UIView *view in pageViewController.view.subviews) {
if ([view isKindOfClass:UIScrollView.class]) {
UIScrollView *scrollView = (UIScrollView *)view;
[scrollView setScrollEnabled:enabled];
return;
}
}
}
For those who are using swift instead of objective-c, here is Squikend's solution transposed.
func findScrollView(#enabled : Bool) {
for view in self.view.subviews {
if view is UIScrollView {
let scrollView = view as UIScrollView
scrollView.scrollEnabled = enabled;
} else {
println("UIScrollView does not exist on this View")
}
}
}
Everyone is complicating this very very much.
This is the whole function you need to disable or enable the scroll.
func enableScroll(_ enable: Bool) {
dataSource = enable ? self : nil
}
Swift 4.2 Version of the answer
func findScrollView(enabled: Bool) {
for view in self.view.subviews {
if view is UIScrollView {
let scrollView = view as! UIScrollView
scrollView.isScrollEnabled = enabled
} else {
print("UIScrollView does not exist on this View")
}
}
}
then yourpagecontorller.findScrollView(enabled: false)
You may also disable gesture recognizers.
for (UIGestureRecognizer *recognizer in pageViewController.gestureRecognizers)
{
recognizer.enabled = NO;
}
Unlike the popular answer, which relies on existing of the inner UIScrollView, this one uses public gestureRecognizers array. The underlying scroll view may not exist if you use book-style paging.

Resources