Xamarin.iOS 中的地图

地图是所有新式移动操作系统的常见功能。 iOS 通过 Map Kit 框架原生提供地图支持。 借助 Map Kit,应用可以轻松添加丰富的交互式地图。 可以通过多种方式自定义这些地图,例如添加注释以标记地图上的位置,以及叠加任意形状的图形。 Map Kit 甚至内置支持显示设备的当前位置。

添加地图

将地图添加到应用程序是通过将 MKMapView 实例添加到视图层次结构来实现的,如下所示:

// map is an MKMapView declared as a class variable
map = new MKMapView (UIScreen.MainScreen.Bounds);
View = map;

MKMapView 是显示地图的 UIView 子类。 只需使用上面的代码添加地图即可生成交互式地图:

示例地图

地图样式

MKMapView 支持 3 种不同样式的地图。 若要应用地图样式,只需将 MapType 属性设置为 MKMapType 枚举中的值即可:

map.MapType = MKMapType.Standard; //road map
map.MapType = MKMapType.Satellite;
map.MapType = MKMapType.Hybrid;

以下屏幕截图显示了可用的不同地图样式:

此屏幕截图显示了可用的不同地图样式

平移和缩放

MKMapView 包括对地图交互功能的支持,例如:

  • 通过捏合手势进行缩放
  • 通过平移手势进行平移

只需设置 MKMapView 实例的 ZoomEnabledScrollEnabled 属性即可启用或禁用这些功能,其中两者的默认值为 true。 例如,若要显示静态地图,只需将相应的属性设置为 false:

map.ZoomEnabled = false;
map.ScrollEnabled = false;

用户位置

除了用户交互之外,MKMapView 还内置支持显示设备位置。 它使用 Core Location 框架来实现此目的。 在访问用户的位置之前,必须先提示用户。 为此,请创建 CLLocationManager 的实例并调用 RequestWhenInUseAuthorization

CLLocationManager locationManager = new CLLocationManager();
locationManager.RequestWhenInUseAuthorization();
//locationManager.RequestAlwaysAuthorization(); //requests permission for access to location data while running in the background

请注意,在 8.0 之前的 iOS 版本中,尝试调用 RequestWhenInUseAuthorization 会导致错误。 如果你想要支持 8 之前的版本,请务必在调用之前检查 iOS 版本。

访问用户位置还需要对 Info.plist 进行修改。 应设置如下与位置数据相关的键:

  • NSLocationWhenInUseUsageDescription - 用于用户在和你的应用互动时访问用户的位置。
  • NSLocationAlwaysUsageDescription - 用于应用在后台访问用户的位置时。

可以通过打开 Info.plist 并选择编辑器底部的“源”来添加这些键

更新 Info.plist 并提示用户授予访问其位置的权限后,可以通过将 ShowsUserLocation 属性设置为 true,在地图上显示用户的位置:

map.ShowsUserLocation = true;

允许位置访问警报

批注

MKMapView 还支持在地图上显示图像(称为注释)。 这些可以是自定义图像或系统定义的各种颜色的图钉。 例如,以下屏幕截图显示了带有图钉和自定义图像的地图:

此屏幕截图显示了具有图钉和自定义图像的地图

添加注释

注释本身有两部分:

  • MKAnnotation 对象,其中包含有关注释的模型数据,例如注释的标题和位置。
  • MKAnnotationView,其中包含要显示的图像以及用户点击注释时显示的(可选)标注。

Map Kit 使用 iOS 委托模式向地图添加注释,其中 MKMapViewDelegate 属性设置为 MKMapViewDelegate 的实例。 该委托的实现负责返回注释的 MKAnnotationView

若要添加注释,首先请通过在 MKMapView 实例上调用 AddAnnotations 来添加注释:

// add an annotation
map.AddAnnotations (new MKPointAnnotation (){
    Title="MyAnnotation",
    Coordinate = new CLLocationCoordinate2D (42.364260, -71.120824)
});

当注释的位置在地图上可见时,MKMapView 将调用其委托的 GetViewForAnnotation 方法来显示 MKAnnotationView

例如,以下代码返回系统提供的 MKPinAnnotationView

string pId = "PinAnnotation";

public override MKAnnotationView GetViewForAnnotation (MKMapView mapView, NSObject annotation)
{
    if (annotation is MKUserLocation)
        return null;

    // create pin annotation view
    MKAnnotationView pinView = (MKPinAnnotationView)mapView.DequeueReusableAnnotation (pId);

    if (pinView == null)
        pinView = new MKPinAnnotationView (annotation, pId);

    ((MKPinAnnotationView)pinView).PinColor = MKPinAnnotationColor.Red;
    pinView.CanShowCallout = true;

    return pinView;
}

重用注释

为了节省内存,MKMapView 允许组建注释视图池以供重用,这与重用表单元格的方式类似。 通过调用 DequeueReusableAnnotation 从池中获取注释视图:

MKAnnotationView pinView = (MKPinAnnotationView)mapView.DequeueReusableAnnotation (pId);

显示标注

如前所述,注释可以选择性地显示标注。 若要显示标注,只需在 MKAnnotationView 上将 CanShowCallout 设置为 true。 这会导致点击注释时显示注释的标题,如下所示:

正在显示的批注标题

自定义标注

还可以自定义标注以显示左右附件视图,如下所示:

pinView.RightCalloutAccessoryView = UIButton.FromType (UIButtonType.DetailDisclosure);
pinView.LeftCalloutAccessoryView = new UIImageView(UIImage.FromFile ("monkey.png"));

此代码会生成以下标注:

示例标注

若要处理用户点击正确附件的场景,只需在 MKMapViewDelegate 中实现 CalloutAccessoryControlTapped 方法即可:

public override void CalloutAccessoryControlTapped (MKMapView mapView, MKAnnotationView view, UIControl control)
{
    ...
}

覆盖

在地图上对图形进行分层的另一种方法是使用叠加层。 叠加层支持绘制在地图缩放时随之缩放的图形内容。 iOS 提供对多种类型的叠加层的支持,包括:

  • 多边形 - 通常用于突出显示地图上的某些区域。
  • 折线- 显示路线时经常出现。
  • 圆圈 - 用于突出显示地图的圆形区域。

此外,还可以创建自定义叠加层,以使用精细的自定义绘图代码演示任意几何图形。 例如,气象雷达是自定义叠加层的适当候选项。

添加叠加层

与注释类似,添加叠加层涉及两个部分:

  • 为叠加层创建模型对象并将其添加到 MKMapView
  • MKMapViewDelegate 中为叠加层创建视图。

叠加层的模型可以是任何 MKShape 子类。 Xamarin.iOS 分别通过 MKPolygonMKPolylineMKCircle 类包括多边形、折线和圆的 MKShape 子类。

例如,以下代码用于添加 MKCircle

var circleOverlay = MKCircle.Circle (mapCenter, 1000);
map.AddOverlay (circleOverlay);

叠加层的视图是一个 MKOverlayView 实例,由 MKMapViewDelegate 中的 GetViewForOverlay 返回。 每个 MKShape 都有一个相应的 MKOverlayView,它知道如何显示给定的形状。 对于 MKPolygon,提供了 MKPolygonView。 同样,MKPolyline 对应于 MKPolylineView,而对于 MKCircle,则提供了 MKCircleView

例如,以下代码为 MKCircle 返回 MKCircleView

public override MKOverlayView GetViewForOverlay (MKMapView mapView, NSObject overlay)
{
    var circleOverlay = overlay as MKCircle;
    var circleView = new MKCircleView (circleOverlay);
    circleView.FillColor = UIColor.Blue;
    return circleView;
}

这会在地图上显示一个圆,如下所示:

地图上显示的圆圈

iOS 包含带有 Map Kit 的本地搜索 API,它允许异步搜索指定地理区域中的兴趣点。

若要执行本地搜索,应用程序必须遵循以下步骤:

  1. 创建 MKLocalSearchRequest 对象。
  2. MKLocalSearchRequest 创建一个 MKLocalSearch 对象。
  3. MKLocalSearch 对象调用 Start 方法。
  4. 检索回调中的 MKLocalSearchResponse 对象。

本地搜索 API 本身不提供用户界面。 它甚至不需要使用地图。 但是,若要实际使用本地搜索,应用程序需要提供某种方法来指定搜索查询并显示结果。 此外,由于结果将包含位置数据,因此在地图上显示它们通常是有意义的。

添加本地搜索 UI

接受搜索输入的一种方法是使用 UISearchBar,它由 UISearchController 提供,并将在表中显示结果。

以下代码将 UISearchController(具有搜索栏属性)添加到 MapViewControllerViewDidLoad 方法中:

//Creates an instance of a custom View Controller that holds the results
var searchResultsController = new SearchResultsViewController (map);

//Creates a search controller updater
var searchUpdater = new SearchResultsUpdator ();
searchUpdater.UpdateSearchResults += searchResultsController.Search;

//add the search controller
searchController = new UISearchController (searchResultsController) {
                SearchResultsUpdater = searchUpdater
            };

//format the search bar
searchController.SearchBar.SizeToFit ();
searchController.SearchBar.SearchBarStyle = UISearchBarStyle.Minimal;
searchController.SearchBar.Placeholder = "Enter a search query";

//the search bar is contained in the navigation bar, so it should be visible
searchController.HidesNavigationBarDuringPresentation = false;

//Ensure the searchResultsController is presented in the current View Controller
DefinesPresentationContext = true;

//Set the search bar in the navigation bar
NavigationItem.TitleView = searchController.SearchBar;

请注意,你需要负责将搜索栏对象合并到用户界面中。 在此示例中,我们将其分配到导航栏的 TitleView,但如果在应用中不使用导航控制器,则必须找到另一个位置来显示它。

在此代码片段中,我们创建了另一个自定义视图控制器 – searchResultsController – 用于显示搜索结果,然后使用该对象来创建搜索控制器对象。 我们还创建了一个新的搜索更新程序,当用户与搜索栏交互时,该更新程序就会激活。 它会接收有关每次按键搜索的通知,并负责更新 UI。 本指南稍后将介绍如何实现 searchResultsControllersearchResultsUpdater

这会导致地图上显示一个搜索栏,如下所示:

在地图上层显示的搜索栏

显示搜索结果

若要显示搜索结果,我们需要创建一个自定义视图控制器;通常是 UITableViewController。 如上所示,searchResultsController 在创建时会传递给 searchController 的构造函数。 以下代码示例演示如何创建此自定义视图控制器:

public class SearchResultsViewController : UITableViewController
{
    static readonly string mapItemCellId = "mapItemCellId";
    MKMapView map;

    public List<MKMapItem> MapItems { get; set; }

    public SearchResultsViewController (MKMapView map)
    {
        this.map = map;

        MapItems = new List<MKMapItem> ();
    }

    public override nint RowsInSection (UITableView tableView, nint section)
    {
        return MapItems.Count;
    }

    public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
    {
        var cell = tableView.DequeueReusableCell (mapItemCellId);

        if (cell == null)
            cell = new UITableViewCell ();

        cell.TextLabel.Text = MapItems [indexPath.Row].Name;
        return cell;
    }

    public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
    {
        // add item to map
        CLLocationCoordinate2D coord = MapItems [indexPath.Row].Placemark.Location.Coordinate;
        map.AddAnnotations (new MKPointAnnotation () {
            Title = MapItems [indexPath.Row].Name,
            Coordinate = coord
        });

        map.SetCenterCoordinate (coord, true);

        DismissViewController (false, null);
    }

    public void Search (string forSearchString)
    {
        // create search request
        var searchRequest = new MKLocalSearchRequest ();
        searchRequest.NaturalLanguageQuery = forSearchString;
        searchRequest.Region = new MKCoordinateRegion (map.UserLocation.Coordinate, new MKCoordinateSpan (0.25, 0.25));

        // perform search
        var localSearch = new MKLocalSearch (searchRequest);

        localSearch.Start (delegate (MKLocalSearchResponse response, NSError error) {
            if (response != null && error == null) {
                this.MapItems = response.MapItems.ToList ();
                this.TableView.ReloadData ();
            } else {
                Console.WriteLine ("local search error: {0}", error);
            }
        });

    }
}

更新搜索结果

SearchResultsUpdater 充当 searchController 的搜索栏和搜索结果之间的中介。

在此示例中,我们必须首先在 SearchResultsViewController 中创建搜索方法。 为此,我们必须创建一个 MKLocalSearch 对象并使用它来发出对 MKLocalSearchRequest 的搜索,结果在传递给 MKLocalSearch 对象的 Start 方法的回调中检索。 然后,结果在包含 MKMapItem 对象数组的 MKLocalSearchResponse 对象中返回:

public void Search (string forSearchString)
{
    // create search request
    var searchRequest = new MKLocalSearchRequest ();
    searchRequest.NaturalLanguageQuery = forSearchString;
    searchRequest.Region = new MKCoordinateRegion (map.UserLocation.Coordinate, new MKCoordinateSpan (0.25, 0.25));

    // perform search
    var localSearch = new MKLocalSearch (searchRequest);

    localSearch.Start (delegate (MKLocalSearchResponse response, NSError error) {
        if (response != null && error == null) {
            this.MapItems = response.MapItems.ToList ();
            this.TableView.ReloadData ();
        } else {
            Console.WriteLine ("local search error: {0}", error);
        }
    });

}

然后,在 MapViewController 中,我们将创建 UISearchResultsUpdating 的自定义实现,将其分配到添加本地搜索 UI 部分中 searchControllerSearchResultsUpdater 属性:

public class SearchResultsUpdator : UISearchResultsUpdating
{
    public event Action<string> UpdateSearchResults = delegate {};

    public override void UpdateSearchResultsForSearchController (UISearchController searchController)
    {
        this.UpdateSearchResults (searchController.SearchBar.Text);
    }
}

上面的实现在从结果中选择一个项目时向地图添加注释,如下所示:

选中结果中的项目时添加到地图中的批注

重要

UISearchController 是在 iOS 8 中实现的。 如果你希望支持低于此版本的设备,则需要使用 UISearchDisplayController

总结

本文探讨了 iOS 的 MapKit 框架。 首先,其中介绍了 MKMapView 类如何允许将交互式地图包含在应用中。 然后演示了如何使用注释和叠加层进一步自定义地图。 最后,探讨了 iOS 6.1 中添加到 Map Kit 中的本地搜索功能,并介绍了如何对兴趣点执行基于位置的查询并将其添加到地图中。