[Flutter/iOS] Geolocator 로 사용자 백그라운드 위치 가져오기 (Feat. Simulator location )
개요
현재 개발중인 프로젝트에서 사용자에게 위치 권한을 요청하고, 현재 위치를 가져와야 하는 기능이 필요해 Flutter Geolocator Plugin을 적용했다.
Geolocator는 다음과 같은 기능을 제공한다:
기기의 마지막 위치와 현재 위치 가져오기
현재 위치 추적
위치 서비스 상태 확인과 권한 요청
두 위치 간 거리 및 방향(방위각) 계산
자세한 내용은 [공식 문서] 참고
이 패키지는 안드로이드, iOS, macOS, Web, Windows에서 모두 활용 가능하지만, 이 글에서는 iOS 설정을 위주로 다뤄보려 한다.
초기 설정 (iOS 기준)
[1] ios/Runner/Info.plist 설정
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location while in use.</string>
<string> 태그 안의 문자열은 사용자가 앱을 처음 실행할 때 위치 접근을 요청하는 팝업에 표시되니 앱 상황에 맞게 적절히 조정하면 된다
나는 백그라운드 위치도 받아와야 하기 때문에 NSLocationWhenInUseUsageDescription 도 설정해줬다. 이걸 설정하는 경우 Apple이 앱 제출 시 이런저런 질문을 할 수 있다고 한다.
특정 메소드들을 사용할 경우 Info.plist에 더 추가 해줘야 하는 것들이 있으니, 문서를 잘 읽어봐야 한다. 위처럼 설정했을 때 기본적인 기능을 사용하는 데는 문제가 없다.
[2] Xcode 설정
Xcode에서 프로젝트를 연다 (플젝 자체를 통으로 열면 안되고 ios 폴더를 열어야 한다)
"Project Targets" → "Runner" → "Sigining & Capabilities 로 이동
"+ Capability를 눌러"Background Modes" 를 선택해준다 (더블클릭)
이후 "Background Modes"에서 Location updates를 체크해준다.
이렇게 설정할 경우, 앱을 앱스토어에 제출할 때 애플에게 사용자 위치 정보가 필요한 이유를 상세히 설명해줘야 한다. 만족할만한 설명을 주지 못하면 앱 제출이 거절될 수 있다 한다.
iOS 시뮬레이터 위치 설정
시뮬레이터에서 기기 위치를 수동으로 원하는 좌표로 설정할 수 있다. 이를 통해 Geolocator의 '사용자 위치 스트리밍' 기능이 잘 작동하는지 확인할 수 있었다.
위치 설정 방법은 다음과 같다:
-
시뮬레이터를 띄워둔 후 위 상단바(..?) 에서 Features → Location → Custom Location
원하는 위도/경도를 설정해준다
테스트 코드
Geolocator의 서비스들이 잘 돌아가는지 확인하기 위한 테스트 페이지를 만들었다.
대충 이런 식으로 생겼다. 버튼을 누르면 적절한 결과값이 밑에 글씨로 반환된다.
(못생김 주의,, 대충 짬 주의,,,)
누군가에겐 도움이 될 수 있지 않을까 싶어 코드를 첨부한다!
// geolocator_test_page.dart
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'dart:async';
import '../geolocator_service.dart';
class GeolocatorTestPage extends StatefulWidget {
const GeolocatorTestPage({Key? key}) : super(key: key);
@override
_GeolocatorTestPageState createState() => _GeolocatorTestPageState();
}
class _GeolocatorTestPageState extends State<GeolocatorTestPage> {
String _output = '버튼 클릭 ㄱㄱ';
List<String> _locationLog = [];
StreamSubscription<Position>? _positionStreamSubscription;
StreamSubscription<ServiceStatus>? _serviceStatusStreamSubscription;
@override
void dispose() {
// Cancel the stream subscriptions when the widget is disposed
_positionStreamSubscription?.cancel();
_serviceStatusStreamSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Geolocator Service Test'),
elevation: 2,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
btn(
onPressed: _getCurrentPosition,
child: '현재 위치 불러오기',
),
btn(
onPressed: _getLastKnownPosition,
child: '마지막으로 알고있는 위치 불러오기',
),
btn(
onPressed: _startListeningToLocationUpdates,
child: 'Location Update Listening 시작',
color: Colors.black,
),
btn(
onPressed: _stopListeningToLocationUpdates,
child: 'Location Update Listening 끝',
),
btn(
onPressed: _checkLocationServiceStatus,
child: 'Location Service 상태 확인',
),
btn(
onPressed: _checkPermissionStatus,
child: '위치 권한 상태 확인 - always 여야함',
),
btn(
onPressed: _requestPermission,
child: 'Permission 요청',
),
btn(
onPressed: _checkLocationAccuracy,
child: 'Location 정확도 확인',
),
btn(
onPressed: _listenToServiceStatusStream,
child: 'Listen to Service Status Changes',
color: Colors.green,
),
btn(
onPressed: _openSettings,
child: '위치 권한 수정',
color: Colors.red,
),
SizedBox(height: 20),
Expanded(
child: SingleChildScrollView(
child: Text(
_output,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
SizedBox(height: 20),
Expanded(
child: ListView.builder(
itemCount: _locationLog.length,
itemBuilder: (context, index) {
return Text(
_locationLog[index],
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.green),
);
},
),
),
],
),
),
);
}
void _setOutput(String text) {
setState(() {
_output = text;
});
}
void _logLocation(String text) {
setState(() {
_locationLog.add(text);
});
}
Future<void> _getCurrentPosition() async {
try {
Position position = await GeolocatorService.getCurrentPosition();
_setOutput(
'Current Position: ${position.latitude}, ${position.longitude}');
} catch (e) {
_setOutput('Error: $e');
}
}
Future<void> _getLastKnownPosition() async {
try {
Position? position = await GeolocatorService.getLastKnownPosition();
if (position != null) {
_setOutput(
'Last Known Position: ${position.latitude}, ${position.longitude}');
} else {
_setOutput('No Last Known Position available.');
}
} catch (e) {
_setOutput('Error: $e');
}
}
void _startListeningToLocationUpdates() {
_positionStreamSubscription = GeolocatorService.getPositionStream(
accuracy: LocationAccuracy.high,
distanceFilter: 10,
).listen(
(Position position) {
final positionString =
'Position Update: ${position.latitude}, ${position.longitude}';
_setOutput(positionString);
_logLocation(positionString);
debugPrint(
'Position Update: ${position.latitude}, ${position.longitude}');
},
onError: (e) {
_setOutput('Error: $e');
},
onDone: () {
_setOutput('Location stream closed.');
},
);
}
void _stopListeningToLocationUpdates() {
_positionStreamSubscription?.cancel();
_setOutput('Stopped listening to location updates');
}
Future<void> _checkLocationServiceStatus() async {
bool isEnabled = await GeolocatorService.isLocationServiceEnabled();
_setOutput('Location Service Enabled: $isEnabled');
}
Future<void> _checkPermissionStatus() async {
LocationPermission permission = await GeolocatorService.checkPermission();
_setOutput('Location Permission Status: $permission');
}
Future<void> _requestPermission() async {
LocationPermission permission = await GeolocatorService.requestPermission();
_setOutput('Permission Requested: $permission');
}
Future<void> _openSettings() async {
bool openSet = await GeolocatorService.openAppSettings();
_setOutput('openSettings: $openSet');
}
Future<void> _checkLocationAccuracy() async {
LocationAccuracyStatus accuracyStatus =
await GeolocatorService.getLocationAccuracy();
_setOutput('Location Accuracy: $accuracyStatus');
}
void _listenToServiceStatusStream() {
_serviceStatusStreamSubscription =
GeolocatorService.getServiceStatusStream().listen(
(ServiceStatus status) {
_setOutput('Service Status: $status');
_logLocation('Service Status: $status');
},
onError: (e) {
_setOutput('Error: $e');
},
);
}
Widget btn({
required VoidCallback onPressed,
required String child,
Color? color,
}) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: color,
),
onPressed: onPressed,
child: Text(
child,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
}
위는 UI 코드, 밑은 로직 코드이다
// geolocator_service.dart
import 'package:flutter/foundation.dart';
import 'package:geolocator/geolocator.dart';
class GeolocatorService {
static Future<Position> getCurrentPosition(
{LocationAccuracy accuracy = LocationAccuracy.high}) async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
}
if (permission == LocationPermission.deniedForever) {
return Future.error('Location permissions are permanently denied');
}
return await Geolocator.getCurrentPosition(desiredAccuracy: accuracy);
}
static Future<Position?> getLastKnownPosition() async {
return await Geolocator.getLastKnownPosition();
}
static Stream<Position> getPositionStream(
{LocationAccuracy accuracy = LocationAccuracy.high,
int distanceFilter = 100}) {
final LocationSettings locationSettings =
LocationSettingsConfig.getPlatformLocationSettings();
return Geolocator.getPositionStream(locationSettings: locationSettings);
}
static Future<bool> isLocationServiceEnabled() async {
return await Geolocator.isLocationServiceEnabled();
}
static Future<LocationPermission> checkPermission() async {
return await Geolocator.checkPermission();
}
static Future<LocationPermission> requestPermission() async {
return await Geolocator.requestPermission();
}
static Future<bool> openAppSettings() async {
return await Geolocator.openAppSettings();
}
// (iOS 14+ and Android 만 적용 가능)
static Future<LocationAccuracyStatus> getLocationAccuracy() async {
return await Geolocator.getLocationAccuracy();
}
static Stream<ServiceStatus> getServiceStatusStream() {
return Geolocator.getServiceStatusStream();
}
}
class LocationSettingsConfig {
static LocationSettings getPlatformLocationSettings() {
if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
return AppleSettings(
accuracy: LocationAccuracy.high,
activityType: ActivityType.fitness,
distanceFilter: 100,
pauseLocationUpdatesAutomatically: false,
showBackgroundLocationIndicator: true,
);
} else if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 100,
forceLocationManager: true,
intervalDuration: const Duration(seconds: 10),
foregroundNotificationConfig: const ForegroundNotificationConfig(
notificationText: "App is tracking your location in the background.",
notificationTitle: "Background Location Tracking",
enableWakeLock: true,
),
);
} else {
return LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 100,
);
}
}
}
Geolocator는 플러터로 위치, 지도 기반 애플리케이션을 개발한다면 필수로 설정해야하는 패키지이지 않을까 싶다. 이런 유용하고 편한 패키지가 있음에 안도했다!
Subscribe to my newsletter
Read articles from Cherrie Kim directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by