108 iOS MapView, 음악재생하기
** iOS 의 CLLocationManager
=>현재 위치 정보를 가져오는 것이 가능
=>나침반 정보를 가져오는 것도 가능
=>영역에 들어오고 나가는 것을 감시하는 것도 가능
=>Beacon 관련 기능 제공
=>사용자의 현재 위치 정보를 서버에 저장하는 것은 인가를 받아야 하는 사항입니다.
**Map View
1.모바일에서 지도를 출력하는 방법
=>iOS에서는 Apple이 제공하는 MapView 사용
=>디바이스 제공하는 기본 Map Application 이용
=>Open API에서 제공하는 Map 을 이용 - Google, Kakao, Naver, SK 등
=>대한민국은 국외에 등록된 업체가 대한민국에서 GIS 사업을 할 수 없도록 되어 있음
2.Geocoding & Reverse Geocoding
=>Geocoding: 주소를 가지고 위치 정보로 변환
Reverse Geocoding: 위치 정보를 가지고 주소로 변환
=>iOS에서는 이 기능을 Apple Server 에서 제공
대한민국에서는 잘 사용하지 않음 - 한글이 잘 안됨
Kakao 나 Google의 API를 주로 이용
3.좌표를 사용할 때 주의할 점
=>위도와 경도 뒤부분의 단위에 주의
=>도 분(60) - 도.소수
**Map View 사용
1.Application 을 생성 - 프로젝트를 생성해도 되고 프로젝트 내에 Target을 생성해도 되고 기존 프로젝트에 ViewController를 추가해도 됩니다.
2.ViewController에 Navigation Controller를 추가
3.ViewController에 툴바와 맵 뷰를 추가
4.툴바에 버튼 2개를 추가 - 확대를 위한 버튼과 맵의 타입을 변경하기 위한 버튼
5.맵 뷰에는 mapView 라는 변수를 연결하고 바 버튼에는 메소드(select)를 연결
6.ViewController.swift 파일에 MapKit 프레임워크를 import(WebView를 사용할 때도 WebKit을 import)
7.실행해서 지도가 출력되는지 확인
8.현재 위치를 지도에 표시
1)CoreLocation을 import
2)위치 정보 사용을 위한 CLLocationManager 클래스의 참조를 저장할 변수를 인스턴스 변수로 선언
//위치 정보 사용 객체의 참조를 저장할 변수
var locationManager : CLLocationManager!
3)viewDidLoad 메소드에서 위치 정보 객체를 생성하고 권한을 요청
//위치 정보 객체 생성
locationManager = CLLocationManager()
//위치정보 사용권한 요청 - 실행 중일 때만 권한을 사용
locationManager.requestWhenInUseAuthorization()
4)viewDidLoad 메소드에 현재 위치를 지도에 표시하도록 하는 설정을 추가
//현재 위치를 지도에 표시하도록 설정
mapView.showsUserLocation = true
5)Info.plist 파일에 위치 정보 사용을 할 때 묻는 질문에 대한 설정을 추가
<key>NSLocationWhenInUseUsageDescription</key>
<string>위치정보가 필요합니다.</string>
9.바버튼 아이템을 눌렀을 때 지도를 확대하고 타입을 변경
1)zoom 메소드 구현
@IBAction func zoom(_ sender: Any) {
//현재 위치 가져오기
let userLocation = mapView.userLocation
//현재 위치를 기준으로 영역을 생성
let region = MKCoordinateRegion(center:userLocation.location!.coordinate, latitudinalMeters: 3000,
longitudinalMeters: 3000)
//맵 뷰의 영역을 설정
mapView.setRegion(region, animated: true)
}
2)type 메소드 구현
@IBAction func type(_ sender: Any) {
if mapView.mapType == MKMapType.standard{
mapView.mapType = MKMapType.satellite
}else{
mapView.mapType = MKMapType.standard
}
}
10.현재 위치가 변경되면 맵 뷰도 따라서 이동하도록 설정
1)viewDidLoad 메소드에 MapView의 delegate를 설정
//맵 뷰의 Delegate 설정
mapView.delegate = self
2)MapViewDelegate를 conform 한 extension을 생성하고 사용자의 위치가 변경된 경우 호출되는 메소드를 구현
extension ViewController : MKMapViewDelegate{
//사용자의 위치 정보가 갱신된 경우 호출되는 메소드
func mapView(_ mapView:MKMapView, didUpdate userLocation:MKUserLocation){
mapView.centerCoordinate = userLocation.location!.coordinate
}
}
11.검색
=>MKLocalSearch 라는 클래스를 이용해서 로컬 검색 기능을 제공 - 사용자가 검색어를 입력해서 검색어에 해당하는 데이터를 찾아서 사용할 수 있음
=>검색 요청은 MKLocalSearchRequest 클래스의 인스턴스를 이용하고 결과는 MKMapItem 객체들의 집합으로 리턴
=>비동기적(Handler - CallBack)으로 처리됨
=>한글로 검색하면 검색이 잘 안됨
=>실제 구현을 한다면 Kakao 나 Naver 와 같은 국내 Open API를 이용하는 것이 효율적입니다.
1)ViewController 에서 맵 뷰의 크기를 조절하고 상단에 레이블 과 텍스트 필드를 배치
2)추가한 텍스트 필드에 변수를 연결(searchText)하고 Did End On Exit 이벤트에 메소드 연결(textFieldReturn)
3)ViewController.swift 파일에 검색된 결과를 저장할 배열 생성
//검색된 위치 정보를 저장할 배열을 생성
var matchingItems = [MKMapItem]()
4)리턴 키를 눌렀을 때 호출되는 메소드 구현
@IBAction func textFieldReturn(_ sender: Any) {
//키보드 제거
searchText.resignFirstResponder()
//기존에 존재하던 어노테이션 제거
mapView.removeAnnotations(mapView.annotations)
//검색된 결과도 삭제
matchingItems.removeAll()
//검색 요청 객체 생성
let request = MKLocalSearch.Request()
//검색어 설정
request.naturalLanguageQuery = searchText.text
//검색 범위 설정
request.region = mapView.region
//실제 검색을 수행해 줄 객체를 생성
let search = MKLocalSearch(request: request)
//검색 요청
search.start(completionHandler: {(response:MKLocalSearch.Response!, error:Error!) in
if error != nil{
NSLog("검색 실패")
}else if response.mapItems.count == 0{
NSLog("검색 결과 없음")
}else{
NSLog("검색 결과 존재")
//검색 결과 순회
for item in response.mapItems as [MKMapItem]{
//배열에 검색된 내용을 추가
self.matchingItems.append(item as MKMapItem)
//지도에 출력할 어노테이션 생성
let annotation = MKPointAnnotation()
annotation.coordinate = item.placemark.coordinate
annotation.title = item.name
annotation.subtitle = item.phoneNumber
//지도에 출력
self.mapView.addAnnotation(annotation)
}
}
})
}
12.검색된 결과를 별도의 테이블 뷰에 출력하고 테이블 뷰에서 셀을 선택하면 현재 위치에서 선택한 곳 까지의 경로를 맵에 출력하기
=>MKDirection 이라는 클래스를 이용 - 애플의 서버를 이용
=>이런 기능은 국내 Open API 에서도 제공
1)결과를 출력할 UITableViewController를 상속받는 클래스를 생성
=>ResultListVC
2)main.storyboard 파일에 TableViewController를 배치하고 Class 속성과 Storyboard ID 속성을 수정
3)ViewController를 선택하고 control을 누른 채 드래그를 해서 새로 배치된 뷰 컨트롤러에 떨어뜨리고 push를 선택 : 연결선이 만들어져서 UI 구조 파악이 쉬워 집니다.
4)ResultListVC.swift 파일에 MapKit import
5)ResultListVC.swift 파일에 이전 뷰 컨트롤러부터 데이터를 넘겨받아서 저장할 배열을 생성
var mapItems: [MKMapItem]?
6)ResultListVC.swift 파일에 테이블 뷰 출력 관련 메소드 재정의
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//?? 는 데이터가 nil 이면 뒤의 내용을 리턴
return mapItems?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil{
cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
}
//데이터 찾아와서 출력하기
if let item = mapItems?[indexPath.row]{
cell!.textLabel?.text = item.name
cell!.detailTextLabel?.text = item.phoneNumber
}
return cell!
}
7)ViewController.swift 파일의 텍스트 필드에 리턴키를 눌렀을 때 검색된 데이터를 ResultListVC 에 전달하고 푸시하는 코드를 추가
//결과 출력화면으로 이동
let resultListVC = self.storyboard?.instantiateViewController(withIdentifier: "ResultListVC") as! ResultListVC
resultListVC.mapItems = self.matchingItems
self.navigationController?.pushViewController(resultListVC, animated: true)
8)경로를 보여줄 UIViewController로 부터 상속받는 클래스를 생성 - RouteVC
9)스토리보드에 ViewController를 추가하고 Class 속성과 Storyboard ID 속성을 수정
10)RouteVC 에 MapView를 추가하고 변수 연결(routeMap)
11)RouteVC.swift 파일에 MapKit import
12)RouteVC.swift 파일에 앞의 뷰 컨트롤러부터 넘겨받을 데이터 변수를 선언
var destination : MKMapItem?
13)현재 위치 정보를 사용하기 위한 변수를 RouteVC.swift 파일에 선언
var locationManager = CLLocationManager()
var userLocation : CLLocation?
14)RouteVC.swift 파일의 viewDidLoad 메소드에 변수 초기화 및 delegate 설정 코드를 추가
//맵 뷰 설정
routeMap.delegate = self
routeMap.showsUserLocation = true
//위치정보 객체 설정
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
locationManager.requestLocation()
15)RouteVC.swift 파일에 extension을 추가하고 Delegate 메소드 구현
extension RouteVC : MKMapViewDelegate, CLLocationManagerDelegate{
//위치 정보가 업데이트 되었을 때 호출되는 메소드
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
//업데이트 된 위치 정보를 저장
userLocation = locations[0]
//지도 갱신하는 사용자 정의 메소드 호출
self.getDirections()
}
//위치 정보가 업데이트 실패 했을 때 호출되는 메소드
func locationManager(_ manager: CLLocationManager, didFailWithError error : Error) {
NSLog(error.localizedDescription)
}
//맵 뷰에 출력을 해주는 메소드
func mapView(_ mapView:MKMapView, rendererFor overlay:MKOverlay)->MKOverlayRenderer{
let renderer = MKPolylineRenderer(overlay:overlay)
renderer.strokeColor = UIColor.green
renderer.lineWidth = 7
return renderer
}
}
16)RouteVC.swift 파일에 경로를 표시하는 사용자 정의 메소드를 2개 추가
=>RouteVC 클래스에 추가해도 되고 extension에 추가해도 됩니다.
//사용자 정의 메소드
func showRoute(_ response : MKDirections.Response){
for route in response.routes{
//경로 표시
routeMap.addOverlay(route.polyline,
level:MKOverlayLevel.aboveRoads)
//움직일 때 마다 경로를 콘솔에 출력
for step in route.steps{
NSLog(step.instructions)
}
}
if let coordinate = userLocation?.coordinate{
//사용자 위치 정보를 기준으로 지도를 다시 표시
let region = MKCoordinateRegion(center:coordinate,
latitudinalMeters: 5000,
longitudinalMeters:5000)
//지도에 표시
routeMap.setRegion(region, animated: true)
}
}
//사용자 정의 메소드
func getDirections(){
//요청 객체 생성 - 경로 탐색
let request = MKDirections.Request()
//위치 설정
request.source = MKMapItem.forCurrentLocation()
//목적지 설정
if let destination = self.destination{
request.destination = destination
}
//옵션 설정
request.requestsAlternateRoutes = false
//요청을 이용해서 경로 객체를 생성
let directions = MKDirections(request: request)
//경로 계산하는 메소드 호출
directions.calculate(completionHandler: {(response, error) in
if error != nil{
NSLog("경로 탐색 실패")
}else{
//response가 nil 이면 예외가 발생
//self.showRoute(response!)
//response가 nil 이면 코드를 수행하지 않음
if let response = response{
self.showRoute(response)
}
}
})
}
17)ResultListVC.swift 파일에 셀을 선택했을 때 호출되는 메소드를 재정의
=>RouteVC에게 데이터를 넘겨서 출력
//셀을 선택했을 때 호출되는 메소드
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//선택한 셀에 해당하는 데이터를 찾아옴
let mapItem = mapItems?[indexPath.row]
let routeVC = self.storyboard?.instantiateViewController(withIdentifier: "RouteVC") as! RouteVC
routeVC.destination = mapItem
self.navigationController?.pushViewController(routeVC, animated: true)
}
12.Flyover
=>3D 형태로 맵을 회전 시키는 기능
1)RouteVC에 네비게이션 바의 오른쪽에 바 버튼 아이템을 추가하고 메소드를 연결(animateCamera)
2)RouteVC.swift 파일에 Flyover 구현을 위한 인스턴스 변수를 선언
//Flyover 관련 변수 선언
let distance : CLLocationDistance = 650
let pitch:CGFloat = 65
let heading = 0.0
var camera:MKMapCamera?
3)RouteVC.swift 파일의 viewDidLoad 메소드에 카메라 설정하는 코드를 추가
//카메라 설정
routeMap.mapType = .hybridFlyover
//좌표 설정
var coordinate : CLLocationCoordinate2D?
if userLocation != nil{
coordinate = userLocation!.coordinate
}else{
coordinate = CLLocationCoordinate2D(latitude: 37.4, longitude: 127.02)
}
camera = MKMapCamera(lookingAtCenter: coordinate!, fromDistance: distance, pitch: pitch, heading: heading)
routeMap.camera = camera!
4)RouteVC.swift 파일의 바버튼 클릭 이벤트 핸들러 작성
@IBAction func animateCamera(_ sender: Any) {
UIView.animate(withDuration: 20, animations: {
self.camera!.heading += 180
self.camera?.pitch = 25
self.routeMap.camera = self.camera!
})
}
13.종로에 접근하면 맵 뷰 위에 이미지를 출력하고 종로에서 벗어나면 이미지를 제거
1)출력할 이미지를 프로젝트에 추가
2)ViewController.swift 파일에 위치 정보를 사용하기 위해서 CoreLocation을 import
import CoreLocation
3)ViewController.swift 파일에 영역에 들어오고 나감을 감시하기 위한 변수를 선언
//영역 객체를 저장할 변수
var region : CLCircularRegion!
//지도에 출력할 이미지 뷰
var couponView:UIImageView!
4)ViewController.swift 파일에 영역에 들어오고 나감을 감시하기 위한 초기 설정을 추가
//영역 설정
let center = CLLocationCoordinate2D(latitude: 37.5690886, longitude: 126.984652)
let maxDistance = 1000.0
region = CLCircularRegion(center: center, radius: maxDistance, identifier: "종로")
//위치 정보 옵션 설정
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
//영역 감시를 시작
locationManager.startMonitoring(for: region)
//이미지 뷰 생성
couponView = UIImageView(image: UIImage(named: "coupon.png"))
couponView.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
5)extension에 CLLocationManagerDelegate 를 추가하고 영역 감시 관련 메소드를 작성
extension ViewController : MKMapViewDelegate, CLLocationManagerDelegate{
//사용자의 위치 정보가 갱신된 경우 호출되는 메소드
func mapView(_ mapView:MKMapView, didUpdate userLocation:MKUserLocation){
mapView.centerCoordinate = userLocation.location!.coordinate
}
//영역 안에 들어오면 호출되는 메소드
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
mapView.addSubview(couponView)
}
//영역에서 벗어나면 호출되는 메소드
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
couponView.removeFromSuperview()
}
}
** MultiMedia
1.미디어 파일의 위치
1)미디어 라이브러리 : 내장 벨소리, 녹음한 소리, 카메라가 촬영한 사진이나 비디오
2)번들: 프로젝트 내에 저장
3)도큐먼트: 앱이 시작될 때 다운로드를 받거나 번들에 복사
4)스트리밍 : 실시간 다운로드 및 재생
2.권장 라이브러리
1)아이팟 라이브러리 내의 음원 재생: MediaPlayer
2)아이팟 라이브러리 내의 비디오 재생: AVKit
3)일반 음악파일이나 스트리밍: AVFoundation
4)비디오 파일이나 스트리밍: AVKit/AVFoundation
5)녹음 이나 녹화: AVFoundation
3.Audio Framework
1)MediaPlayer: 음악, 오디오 북, 오디오 팟 캐스트 등과 같은 리소스 들을 플레이하기 위해서 사용
2)AVFoundation: 음악을 재생하거나 녹음 그리고 AVAnimation 이나 캡쳐된 아이템을 플레이할 때 사용
3)AudioToolbox: 패킷을 통해 전달되는 오디오나 오디오 포맷을 변경할 때 사용
4)Open AL: 게임같은 곳에서 사용하는 오디오 데이터를 가공할 때 사용
iOS는 Open AL 1.1 버전을 지원
4.음악 파일과 내장 사운드 재생
=>AVFoundation이 제공하는 AVAudioPlayer 클래스 이용
객체 생성은 URL을 이용해서 생성할 수 있고 Data를 이용해서 생성 가능
=>AVAudioPlayer 클래스에는 음원재생과 관련된 속성과 메소드가 존재하고 이벤트 처리는 AVAudioPlayerDelegate 프로토콜에 있음
=>내장 사운드는 System/Library/Audio/UISounds 디렉토리에 존재
내장 사운드를 재생하고자 하면 내장 사운드를 도큐먼트 디렉토리에 복사해서 재생하면 됩니다.
1)프로젝트 생성
2)재생할 사운드 파일을 프로젝트에 추가
3)ViewController에 버튼 2개와 슬라이더 1개를 배치
4)버튼은 touchUpInside 이벤트에 메소드를 연결하고 슬라이더는 valueChanged 이벤트에 메소드 연결 - audioPlay, audioStop, changeVolumn
5)ViewController.swift 파일에 AVFoundation을 import
6)ViewController.swift 파일에 오디오 재생을 위한 AVAudioPlayer 변수를 추가
//오디오 재생을 위한 객체의 참조를 저장하기 위한 변수
var audioPlayer : AVAudioPlayer?
7) ViewController.swift 파일의 viewDidLoad 메소드에서 AVAudioPlayer 초기화 작업
8) 시작 버튼을 눌렀을 때 호출할 메소드 구현
@IBAction func audioPlay(_ sender: Any) {
audioPlayer?.play()
}
9) 중지 버튼을 눌럿을 때 호출될 메소드 구현
@IBAction func audioStop(_ sender: Any) {
audioPlayer?.stop()
}
10) 슬라이더의 값이 변경될 때 호출되는 메소드 구현
@IBAction func changeVolumn(_ sender: Any) {
let slider = sender as! UISlider
audioPlayer?.volume = slider.value
}
여기까지 작성하고 테스트 하면 시작 버튼을 누르면 음악이 재생된다. 이 때 앱을 중지하면 음악도 같이 중지된다.(백그라운드 재생은 아직 안되는 상태)
5. 백그라운드 음원 재생
=> iOS 에서는 3가지 작업만 백그라운드 작업이 가능
음원 재생, 위치 정보 가져오기, 네트워크(Newsstand, Bluetooth, 다운로드, APNS, External accessory)
=> 백그라운드 모드 설정 - Target에서 Capabilities 에서 Background Modes를 추가해서 설정한다.
오디오는 세션 설정을 해야하고 네트워크는 스레드를 이용해서 작성
(세션이란 웹 사이트의 여러 페이지에 걸체 사용되는 사용자 정보를 저장하는 방법을 의미한다.)
1) 백그라운드에서 음원을 재생할 수 있도록 설정
=> Target을 선택하고 Capabilities 에서 설정
2) 시작 버튼을 눌렀을 때 호출되는 메소드를 수정
//백그라운드에서 오디오 재생이 이어지도록 설정
let session = AVAudioSession.sharedInstance()
do{
try session.setCategory(AVAudioSession.Category.playback, mode: .default, policy: AVAudioSession.RouteSharingPolicy.longFormAudio, options: [])
}catch let error{
NSLog(error.localizedDescription)
}
=> background에서 음원 재생을 하는 경우 ToDay Extension 을 만드는 것을 고려
=> 음원이나 비디오 재생은 이어폰의 영향도 받는다.
AVAudioSeession의 AVAudioSessionRouteChangeNotification을 이용하면 이어폰 연결이 되었을 때 그리고 연결이 해제될 때 호출할 함수를 등록할 수 있다.