演练 - Xamarin.iOS 中的后台位置

在此示例中,我们将生成一个 iOS 位置应用程序,该应用程序将打印有关当前位置的信息:纬度、经度和其他参数到屏幕。 此应用程序将演示如何正确执行位置更新,无论应用程序处于活动状态还是后台。

本演练介绍了一些关键背景概念,包括将应用注册为后台必需的应用程序、在应用背景化时暂停 UI 更新,以及使用 WillEnterBackgroundWillEnterForegroundAppDelegate 方法。

应用程序设置

  1. 首先,创建新的 iOS > 应用 > 单视图应用程序 (C#)。 将其称为“位置”,并确保选中了 iPad 和 iPhone。

  2. 位置应用程序在 iOS 中限定为后台必需的应用程序。 通过编辑项目的 Info.plist 文件,将应用程序注册为 Location 应用程序。

    在“解决方案资源管理器”下,双击 Info.plist 文件将其打开,然后滚动到列表底部。 勾选“启用后台模式”和“位置更新”选项旁的方框。

    在 Visual Studio for Mac 中,它将如下所示:

    勾选“启用后台模式”和“位置更新”选项旁的方框

    在 Visual Studio 中,Info.plist 需要通过添加以下键/值对来手动更新:

    <key>UIBackgroundModes</key>
    <array>
      <string>location</string>
    </array>
    
  3. 注册应用程序后,可以从设备获取位置数据。 在 iOS 中,CLLocationManager 类用于访问位置信息,并可以引发提供位置更新的事件。

  4. 在代码中,创建一个名为 LocationManager 的新类,为各种屏幕和代码提供一个位置来订阅位置更新。 在 LocationManager 类中,将 CLLocationManager 的实例设置为 LocMgr

    public class LocationManager
    {
        protected CLLocationManager locMgr;
    
        public LocationManager () {
            this.locMgr = new CLLocationManager();
            this.locMgr.PausesLocationUpdatesAutomatically = false;
    
            // iOS 8 has additional permissions requirements
            if (UIDevice.CurrentDevice.CheckSystemVersion (8, 0)) {
                locMgr.RequestAlwaysAuthorization (); // works in background
                //locMgr.RequestWhenInUseAuthorization (); // only in foreground
            }
    
            if (UIDevice.CurrentDevice.CheckSystemVersion (9, 0)) {
                locMgr.AllowsBackgroundLocationUpdates = true;
            }
        }
    
        public CLLocationManager LocMgr {
            get { return this.locMgr; }
        }
    }
    

    上述代码对 CLLocationManager 类设置许多属性和权限:

    • PausesLocationUpdatesAutomatically – 这是一个布尔值,可以根据系统是否允许暂停位置更新而进行设置。 在某些设备上,它默认为 true,这可能导致设备在大约 15 分钟后停止获取后台位置更新。
    • RequestAlwaysAuthorization - 应传递此方法,以向用户提供允许在后台访问位置的选项。 如果希望为用户提供允许仅在应用位于前台时访问位置的选项,也可以传递 RequestWhenInUseAuthorization
    • AllowsBackgroundLocationUpdates – 这是 iOS 9 中引入的布尔属性,该属性可以设置为允许应用在挂起时接收位置更新。

    重要

    iOS 8(及更高)还需要 Info.plist 文件中的条目,以将用户显示为授权请求的一部分。

  5. 为应用所需的权限类型 NSLocationAlwaysUsageDescriptionNSLocationWhenInUseUsageDescription 和/或 NSLocationAlwaysAndWhenInUseUsageDescription 添加 Info.plist 键,该字符串将在请求位置数据访问的警报中向用户显示。

  6. iOS 9 要求在使用 AllowsBackgroundLocationUpdates 时,Info.plist 包含具有值 location 的键 UIBackgroundModes。 如果已完成本演练的步骤 2,则应已在 Info.plist 文件中。

  7. LocationManager 类中,使用以下代码创建一个名为 StartLocationUpdates 的方法。 此代码演示如何开始从 CLLocationManager 接收位置更新:

    if (CLLocationManager.LocationServicesEnabled) {
        //set the desired accuracy, in meters
        LocMgr.DesiredAccuracy = 1;
        LocMgr.LocationsUpdated += (object sender, CLLocationsUpdatedEventArgs e) =>
        {
            // fire our custom Location Updated event
            LocationUpdated (this, new LocationUpdatedEventArgs (e.Locations [e.Locations.Length - 1]));
        };
        LocMgr.StartUpdatingLocation();
    }
    

    此方法中发生了几个重要的事情。 首先,我们执行检查以查看应用程序是否有权访问设备上的位置数据。 我们通过对 CLLocationManager 调用 LocationServicesEnabled 来验证这一点。 如果用户拒绝应用程序访问位置信息,此方法将返回 false

  8. 接下来,告知位置管理器更新的频率。 CLLocationManager 提供了许多用于筛选和配置位置数据的选项,包括更新频率。 在此示例中,将 DesiredAccuracy 设置为在按计量更改的位置时更新。 有关配置位置更新频率和其他首选项的详细信息,请参阅 Apple 文档中 CLLocationManager 类参考

  9. 最后,对 CLLocationManager 实例调用 StartUpdatingLocation。 这会告知位置管理器获取当前位置的初始修补程序,并开始发送更新

到目前为止,已创建位置管理器,并配置了要接收的数据类型,并确定初始位置。 现在,代码需要将位置数据呈现到用户界面。 可以使用将 CLLocation 作为参数的自定义事件执行此操作:

// event for the location changing
public event EventHandler<LocationUpdatedEventArgs>LocationUpdated = delegate { };

下一步是从 CLLocationManager 订阅位置更新,并在新位置数据可用时引发自定义 LocationUpdated 事件,以参数的形式传入位置。 为此,请创建新的类 LocationUpdateEventArgs.cs。 此代码可在主应用程序中访问,并在引发事件时返回设备位置:

public class LocationUpdatedEventArgs : EventArgs
{
    CLLocation location;

    public LocationUpdatedEventArgs(CLLocation location)
    {
       this.location = location;
    }

    public CLLocation Location
    {
       get { return location; }
    }
}

用户界面

  1. 使用 Xcode Interface Builder 生成将显示位置信息的屏幕。 双击 Main.storyboard 文件开始。

    在情节提要上,将多个标签拖到屏幕上,充当位置信息的占位符。 在此示例中,有纬度、经度、海拔、路线和速度的标签。

    有关详细信息,请参阅用 Xcode 设计用户界面

  2. 在 Solution Pad 中,双击 ViewController.cs 文件并对其进行编辑以创建 LocationManager 的新实例,并在其上调用 StartLocationUpdates。 更改代码,如下所示:

    #region Computed Properties
    public static bool UserInterfaceIdiomIsPhone {
        get { return UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; }
    }
    
    public static LocationManager Manager { get; set;}
    #endregion
    
    #region Constructors
    public ViewController (IntPtr handle) : base (handle)
    {
    // As soon as the app is done launching, begin generating location updates in the location manager
        Manager = new LocationManager();
        Manager.StartLocationUpdates();
    }
    
    #endregion
    

    这将在应用程序启动时启动位置更新,尽管不会显示任何数据。

  3. 收到位置更新后,请使用位置信息更新屏幕。 以下方法从 LocationUpdated 事件中获取位置,并在 UI 中显示该位置:

    #region Public Methods
    public void HandleLocationChanged (object sender, LocationUpdatedEventArgs e)
    {
        // Handle foreground updates
        CLLocation location = e.Location;
    
        LblAltitude.Text = location.Altitude + " meters";
        LblLongitude.Text = location.Coordinate.Longitude.ToString ();
        LblLatitude.Text = location.Coordinate.Latitude.ToString ();
        LblCourse.Text = location.Course.ToString ();
        LblSpeed.Text = location.Speed.ToString ();
    
        Console.WriteLine ("foreground updated");
    }
    #endregion
    

我们仍然需要订阅 AppDelegate 中的 LocationUpdated 事件,并调用新方法来更新 UI。 在 StartLocationUpdates 调用后立即在 ViewDidLoad, 中添加以下代码:

public override void ViewDidLoad ()
{
    base.ViewDidLoad ();

    // It is better to handle this with notifications, so that the UI updates
    // resume when the application re-enters the foreground!
    Manager.LocationUpdated += HandleLocationChanged;

}

现在,当应用程序运行时,它应如下所示:

示例应用运行

处理活动状态和后台状态

  1. 应用程序在前台处于活动状态时正在输出位置更新。 若要演示应用进入后台时会发生什么情况,请重写跟踪应用程序状态更改的 AppDelegate 方法,以便在应用程序在前台和后台之间切换时写入控制台:

    public override void DidEnterBackground (UIApplication application)
    {
        Console.WriteLine ("App entering background state.");
    }
    
    public override void WillEnterForeground (UIApplication application)
    {
        Console.WriteLine ("App will enter foreground");
    }
    

    LocationManager 中添加以下代码,将更新的位置数据持续输出到应用程序输出,以验证位置信息在后台是否仍然可用:

    public class LocationManager
    {
        public LocationManager ()
        {
        ...
        LocationUpdated += PrintLocation;
        }
        ...
    
        //This will keep going in the background and the foreground
        public void PrintLocation (object sender, LocationUpdatedEventArgs e) {
        CLLocation location = e.Location;
        Console.WriteLine ("Altitude: " + location.Altitude + " meters");
        Console.WriteLine ("Longitude: " + location.Coordinate.Longitude);
        Console.WriteLine ("Latitude: " + location.Coordinate.Latitude);
        Console.WriteLine ("Course: " + location.Course);
        Console.WriteLine ("Speed: " + location.Speed);
        }
    }
    
  2. 代码存在一个剩余的问题:尝试在应用后台时更新 UI 将导致 iOS 终止它。 当应用进入后台时,代码需要取消订阅位置更新并停止更新 UI。

    当应用即将过渡到其他应用程序状态时,iOS 会向我们提供通知。 在这种情况下,我们可以订阅 ObserveDidEnterBackground 通知。

    以下代码片段演示如何使用通知让视图知道何时停止 UI 更新。 这将进入 ViewDidLoad

    UIApplication.Notifications.ObserveDidEnterBackground ((sender, args) => {
        Manager.LocationUpdated -= HandleLocationChanged;
    });
    

    应用运行时,输出将如下所示:

    控制台中位置输出的示例

  3. 应用程序在前台运行时将位置更新到屏幕,并在后台运行时继续将数据打印到应用程序输出窗口。

只有一个未完成的问题仍然存在:屏幕在首次加载应用时启动 UI 更新,但它无法知道应用何时重新进入前台。 如果后台应用程序被带回前台,UI 更新将不会恢复。

若要解决此问题,请在另一个通知内嵌套启动 UI 更新的调用,当应用程序进入活动状态时将触发该调用:

UIApplication.Notifications.ObserveDidBecomeActive ((sender, args) => {
  Manager.LocationUpdated += HandleLocationChanged;
});

现在,UI 将在首次启动应用程序时开始更新,并在应用返回到前台时继续更新。

在本演练中,我们构建了一个行为良好的后台感知 iOS 应用程序,用于将位置数据输出到屏幕和应用程序输出窗口。