우선 백그라운드 미디어 재생까지는 구현된 것으로 가정

MPRemoteCommandCenter

외부 액세서리 및 시스템의 제어를 통한 원격 이벤트에 응답하는 Object

shared() 메서드를 통한 싱글턴으로 사용된다.

공유 인스턴스 검색

class func shared() -> MPRemoteCommandCenter

시스템의 원격 명령 개체에 액세스하는 데 사용하는 공유 개체를 반환합니다.

 

재생 명령

var pauseCommand: MPRemoteCommand

현재 항목의 재생을 일시 중지하기위한 명령 개체입니다.

var playCommand: MPRemoteCommand

현재 항목의 재생을 시작하기위한 명령 개체입니다.

var stopCommand: MPRemoteCommand

현재 항목의 재생을 중지하기위한 명령 개체입니다.

var togglePlayPauseCommand: MPRemoteCommand

현재 항목 재생과 일시 중지 사이를 전환하기위한 명령 개체입니다.

트랙 간 탐색

var nextTrackCommand: MPRemoteCommand

다음 트랙을 선택하기위한 명령 개체입니다.

var previousTrackCommand: MPRemoteCommand

이전 트랙을 선택하기위한 명령 개체입니다.

var changeRepeatModeCommand: MPChangeRepeatModeCommand

반복 모드를 변경하기위한 명령 개체입니다.

var changeShuffleModeCommand: MPChangeShuffleModeCommand

셔플 모드를 변경하기위한 명령 개체입니다.

트랙 내용 탐색

var changePlaybackRateCommand: MPChangePlaybackRateCommand

현재 미디어 항목의 재생 속도를 변경하기위한 명령 개체입니다.

var seekBackwardCommand: MPRemoteCommand

단일 미디어 항목을 통해 뒤로 검색하기위한 명령 개체입니다.

var seekForwardCommand: MPRemoteCommand

단일 미디어 항목을 통해 앞으로 검색하기위한 명령 개체입니다.

var skipBackwardCommand: MPSkipIntervalCommand

미디어 항목의 이전 지점을 재생하기위한 명령 개체입니다.

var skipForwardCommand: MPSkipIntervalCommand

미디어 항목에서 미래 지점을 재생하기위한 명령 개체입니다.

var changePlaybackPositionCommand: MPChangePlaybackPositionCommand

미디어 항목에서 재생 위치를 변경하기위한 명령 개체입니다.

미디어 항목 평가

var ratingCommand: MPRatingCommand

미디어 항목을 평가하기위한 명령 개체입니다.

var likeCommand: MPFeedbackCommand

사용자가 현재 재생중인 것을 좋아함을 나타내는 명령 개체입니다.

var dislikeCommand: MPFeedbackCommand

사용자가 현재 재생중인 항목을 싫어함을 나타내는 명령 개체입니다.

미디어 항목 북마크

var bookmarkCommand: MPFeedbackCommand

사용자가 미디어 항목을 기억하기를 원함을 나타내는 명령 개체입니다.

언어 옵션 활성화

var enableLanguageOptionCommand: MPRemoteCommand

언어 옵션을 활성화하기위한 명령 개체입니다.

var disableLanguageOptionCommand: MPRemoteCommand

언어 옵션을 비활성화하기위한 명령 개체


MPNowPlayingInfoCenter

현재 재생중인 미디어에 대한 현재 정보를 설정하는 Object

The MPNowPlayingInfoCenter object contains a nowPlayingInfo dictionary whose contents describe the item being played.

nowPlayingInfo Dictionary를 사용해서 콘텐츠를 업데이트 할 수 있다.

 

AirPlay도 가능하고, 자동차나 iPod 악세서리에 연결 시 정보가 표시될 수 있다고 한다.

 

MPRemoteCommandCenter와 마찬가지로 싱글톤 객체를 반환한다.

default() 메서드를 사용하여 참조할 수 있다.

기본 Now Playing 정보 센터로 작업하기

class func `default`() -> MPNowPlayingInfoCenter

현재 정보 센터를 재생하는 싱글 톤을 반환합니다.

var nowPlayingInfo: [String : Any]?

현재 재생중인 기본 정보 센터에 대한 현재 재생중인 정보입니다.

enum MPNowPlayingInfoMediaType

현재 재생중인 미디어 유형입니다.

 

지금 재생중인 메타 데이터 속성

현재 재생중인 항목에 대한 구체적인 정보를 제공합니다.

let MPNowPlayingInfoCollectionIdentifier: String

현재 재생중인 항목이 속한 컬렉션의 식별자입니다.

let MPNowPlayingInfoPropertyAvailableLanguageOptions: String

현재 재생중인 항목에 사용할 수있는 언어 옵션 그룹입니다.

let MPNowPlayingInfoPropertyAssetURL: String

현재 재생중인 항목의 기본 자산을 가리키는 URL입니다.

let MPNowPlayingInfoPropertyChapterCount: String

현재 재생중인 항목의 총 챕터 수입니다.

let MPNowPlayingInfoPropertyChapterNumber: String

현재 재생중인 챕터에 해당하는 번호입니다.

let MPNowPlayingInfoPropertyCurrentLanguageOptions: String

현재 재생중인 항목에 대해 현재 활성화 된 언어 옵션입니다.

let MPNowPlayingInfoPropertyDefaultPlaybackRate: String

현재 재생중인 항목의 기본 재생 속도입니다.

let MPNowPlayingInfoPropertyCurrentPlaybackDate: String

현재 경과 된 재생 시간과 관련된 날짜입니다.

let MPNowPlayingInfoPropertyElapsedPlaybackTime: String

현재 재생중인 항목의 경과 시간 (초)입니다.

let MPNowPlayingInfoPropertyExternalContentIdentifier: String

앱을 다시 시작하더라도 현재 재생중인 항목을 고유하게 식별하는 불투명 식별자입니다.

let MPNowPlayingInfoPropertyExternalUserProfileIdentifier: String

앱을 다시 시작하더라도 현재 재생중인 항목이 재생되는 프로필을 고유하게 식별하는 불투명 식별자입니다.

let MPNowPlayingInfoPropertyIsLiveStream: String

현재 재생중인 항목이 실시간 스트림인지 여부를 나타냅니다.

let MPNowPlayingInfoPropertyMediaType: String

지금 재생중인 항목의 미디어 유형입니다.

let MPNowPlayingInfoPropertyPlaybackProgress: String

현재 재생중인 항목의 현재 진행률입니다.

let MPNowPlayingInfoPropertyPlaybackQueueCount: String

앱의 재생 대기열에있는 총 항목 수입니다.

let MPNowPlayingInfoPropertyPlaybackQueueIndex: String

앱의 재생 대기열에서 현재 재생중인 항목의 색인입니다.

let MPNowPlayingInfoPropertyPlaybackRate: String

현재 재생중인 항목의 재생 속도이며 값은 정상 재생 속도  나타냅니다.1.0

let MPNowPlayingInfoPropertyServiceIdentifier: String

현재 재생중인 항목과 관련된 서비스 제공 업체입니다.


기능 구현

뭐 사용할 수 있는 건 많다...

 

우선 사용하는 Application이 Remote Control Events를 처리할 수 있도록 하자.

UIApplication.shared.beginReceivingRemoteControlEvents()

 

나는 재생/일시정지/이전, 다음곡 재생/재생 위치 변경 의 기본적인 스트리밍 혹은 음악 재생 앱들이 사용하는 기능들을 썼다.

		//싱글톤 객체 참조 변수 선언
        let center = MPRemoteCommandCenter.shared()
        
		//playButton 음악 재생
        center.playCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
            self.playSong()
            return .success
        }
        
        //pauseButton 음악 일시정지
        center.pauseCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
            self.pauseSong()
            return .success
        }
        
        //nextTrackButton 다음곡 재생
        center.nextTrackCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
            self.playNext()
            return .success
        }
        
        //prevTrackButton 이전곡 재생
        center.previousTrackCommand.addTarget { (_) -> MPRemoteCommandHandlerStatus in
            self.playPrev()
            return .success
        }
        
        //slider 재생 위치 변경
        center.changePlaybackPositionCommand.addTarget { (event) -> MPRemoteCommandHandlerStatus in
            if let positionTime = (event as? MPChangePlaybackPositionCommandEvent)?.positionTime {
                let seekTime = CMTime(value: Int64(positionTime), timescale: 1)
                self.player.seek(to: seekTime)
            }

            return .success
        }

해당 그림처럼 내가 사용하고자 했던 Command들은 활성화가 된다.

 


UI

그럼 이제 어떤 곡이 재생 중인지 보여야겠다

func updateRemoteCommandInfoCenter() {
        let center = MPNowPlayingInfoCenter.default()
        var nowPlayingInfo = center.nowPlayingInfo ?? [String: Any]()
           
		DispatchQueue.main.async {
          nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: nowPlayingImage.size, requestHandler: { (_) -> UIImage in
          	nowPlayingImage
          })
          //
          nowPlayingInfo[MPMediaItemPropertyTitle] = nowPlayingMusic.songName
          nowPlayingInfo[MPMediaItemPropertyArtist] = nowPlayingMusic.artistName
          nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self.player.currentItem?.asset.duration.seconds
          nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = self.player.rate
          nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.player.currentTime().seconds
          
          //적용한 nowPlayingInfo를 center에 적용
          center.nowPlayingInfo = nowPlayingInfo
        }
}

이미지 사이즈가 맞지 않아 화질이 깨지긴하지만, 쨋든 UI까지 적용했다.

AVPlayer

  • 미디어를 관리하는 데에 사용되는 컨트롤러 Object
  • Video, MP3와 같은 오디오 파일을 재생 가능
    • Local과 Remote File 기반, HTTP을 통한 Streaming 가능
  • 한 번에 하나의 미디어를 재생할 수 있음
  • AVQueuePlayer라는 하위 클래스를 사용하여 대기열을 만들고 관리할 수 있다.
    • Queue의 items() 라는 메소드를 통해 Queue 대기열 아이템(AVPlayerItem)을 받아올 수 있는데,
      스트리밍의 경우 보안을 위해 URL을 무한대로 사용할 수 없는 구조도 있다고 한다.
      따라서, AVQueuePlayer보다는 AVPlayer를 사용하여 직접 Queue 구현을 했다.
  • replaceCurrentItem(with: AVPlayerItem)을 사용하여 현재 관리하는 하나의 미디어를 관리할 수 있다.

AVPlayer 상태 변경 Observe

Delegate가 없나보다. AVAudioPlayer에는 있던 것 같은데... 그렇다고 AVAudioPlayer를 쓰자니 버그인지 내가 모르는 건지 안되는 부분들이 있더라...

 

상태 관찰

KVO 를 사용해서 상태 변화를 알아챌 수 있다.

Main Thread에서 등록/해제 해야함(다른 Thread에서 해도 MainThread에서 호출한다고 한다.)

currentItem, playback rate 등 Observe 가능

 observeValue(forKeyPath:of:change:context:) -> Observe 메소드이다. (KVO는 옵씨 런타임에 의존한다고해서 Swift로 개발할 경우에는 잘 안쓴다고 한다.)

 

나는 현재 재생중인 곡이 끝났는지 확인을 위해 Status Observing이 필요했다.

KVO에 익숙치도 않고 다른 로직들도 NotificationCenter로 작성되어 있어서, NotificationCenter를 이용하는 방법을 찾아보니 방법이 있었다.

NSNotification.Name.AVPlayerItemDidPlayToEndTime 이 있더라, 그래서 썼다..

//NotificationCenter 이용
NotificationCenter.default.addObserver(self, selector: #selector(playingMusicFinish(_:)), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
@objc func playingMusicFinish(_ notification: Notification) {
	//필요한 정보나 객체가 있으면 object를 통해서 받아서 이용하면 된다.       
    //이 메소드는 현재 진행중인 PlayerItem이 EndTime에 도달하면 호출된다.
}

 

시간 상태 관찰

시간은 지속적으로 변하기에 KVO는 부적합하다고 한다. (이부분에 대해서는 따로 공부가 필요할 듯 KVO...)

AVPlayer는 시간 변화를 관찰하기 위해 2가지 메소드를 제공한다.

두 메소드를 이용하여 주기적/경계별로 시간 변경을 관찰할 수 있다.

 

나는 현재 음악이 얼마나 재생되었는지 UISlider에 표시하기 위해 사용했다.

func addPeriodicTimeObserver(closurePerInterval: @escaping (Float) -> Void) {
        let time = CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        
        MusicPlayer.shared.player.addPeriodicTimeObserver(forInterval: time, queue: .main) { progressTime in
            //현재 진행된 progressTime을 '초'로 변경
            let seconds = CMTimeGetSeconds(progressTime)
            //00:00 할 때, '초'/'분'
//            let secondsString = String(format: "%02d", Int(seconds) % 60)
//            let minutesString = String(format: "%02d", Int(seconds / 60))
            
            if let duration = MusicPlayer.shared.player.currentItem?.duration{
                let durationSeconds = CMTimeGetSeconds(duration)
				//UISlider에 적용하기 위해 비율로 변경
                closurePerInterval(Float(seconds / durationSeconds))
            }
        }
    }
addPeriodicTimeObserver { [weak self] currentDuration in
            guard let self = self else { return }
            self.slider.value = currentDuration
        }

음악이 재생되면서 정삭적으로 0.01초 마다 UISlider의 Track이 이동하는 것을 볼 수 있다.

 

 


Background Mode

이전에 Location, Fetch, Processing 사용할 때보다는 진짜 진짜 진짜 훨씬 쉬웠다...

 

do{
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
        }catch{
            print(error.localizedDescription)
        }

정상적으로 background에서도 Audio가 들린다. (실 기기에서는 문제없는데, Simulator로 할 때 노이즈가 낄 때가 있는데 무슨 문제인지는 모르겠다!)

 

Note
You can activate the audio session at any time after setting its category, but it’s generally preferable to defer this call until your app begins audio playback. Deferring the call ensures that you won’t prematurely interrupt any other background audio that may be in progress.

언제든지 카테고리를 설정해도 된다. 하지만 setActive()는 오디오를 재생할 때까지 미루는 것이 좋다고 한다.

 

UITableView

UIScrollView를 상속받은 class이며, 세로 스크롤만 지원하고 주로 동일한 형태의 Cell을 사용하는 List를 구현할 때 사용

여러 열(Column)이나 각각의 셀에서 다양한 모양과 크기 등을 사용하고 싶을 때는 CollectionView를 사용한다.

 

선택(Select)

앱을 개발하며 주로 Cell을 눌렀을 때 동작시키는 UITableViewDelegate의 tableView(_:didSelectRowAt:)을 사용했다.

 

Cell 선택이 가능하면 그만큼 Cell을 선택한 후에 할 수 있는 동작이 여러개

혹은, Multiple Selection이 가능하다는 생각이 든다.

 

물론 자주 사용하지는 않겠지만, 이번에 사용해봤기 때문에 정리를 해봐야겠다.

 

Selection

기본적으로 IB에서는 쉽게 적용할 수 있다

가장 아래에 Selection이 있다.

No Selection / Single Selection / Multiple Selection 지정이 가능하다.

 

물론, 코드로도 지정할 수 있다.

tableView.allowsSelection = 
tableView.allowsMultipleSelection = 

두개의 프로퍼티를 true/false로 지정할 수 있다.

 

전체 선택

//단일 Section이라 가정
for row in 0..<tableView.numberOfRows(inSection: 0) {
	tableView.selectRow(at: IndexPath(row: row, section: 0), animated: true, scrollPosition: .none)
}

tableView에 전체 선택 API가 있을 줄 알았는데, 없더라... 일일이 select하고싶은 indexPath를 갖고 선택해준다.

 

전체 해제

// 변수명 그대로 indexPathsForSelectedRows -> 선택된 Row들의 indexPath들
/// 이런게 바로 클린 코드인가... 변수명만 봐도 무슨 역할인지 알겠네...
tableView.indexPathsForSelectedRows?.forEach({ indexPath in
	tableView.deselectRow(at: indexPath, animated: true)
})

 

삽입

맨 상단에 새로운 Cell을 삽입해보자.

코드는 정말 간단하다.

//데이터 추가 및 작업
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)

 

 

삭제

//데이터 삭제 및 작업
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)

 

이동

마지막에 있는 Cell을 가장 위로 올려보겠다.

 

tableView.moveRow(at: IndexPath(row: count-1, section: 0), to: IndexPath(row: 0, section: 0))

 

 

 

동시에 여러 애니메이팅(삽입 삭제 이동) 작업

방법은 2가지가 있다.

  1. performBatchUpdates() 사용
  2. performBatchUpdates() 미 사용

ㅋㅋㅋ... 진짜임...

 

최 하단 2개의 Cell을 최상단으로 올릴 것이다!!

 

우선 사용했을 때 어떻게 되는 지 보자.

 

사용시

 

미 사용시

 

똑같이 보일 수도 있는데... 다르다

숫자 4를 보자... 위치가 다르다..! 왜 다른 걸까..?

코드를 보자

 

@IBAction func movesUsingBatch(_ sender: Any) {
    tableView.performBatchUpdates {
	    tableView.moveRow(at: IndexPath(row: count-1, section: 0), to: IndexPath(row: 0, section: 0))
		tableView.moveRow(at: IndexPath(row: count-2, section: 0), to: IndexPath(row: 1, section: 0))
	} completion: { _ in
		print("")
	}
}
@IBAction func moves(_ sender: Any) {
	tableView.moveRow(at: IndexPath(row: count-1, section: 0), to: IndexPath(row: 0, section: 0))
	tableView.moveRow(at: IndexPath(row: count-1, section: 0), to: IndexPath(row: 0, section: 0))
}

위에는 count-1 -> 0 , count-2 -> 1 로 이동했다.

아래는 똑같이 count-1 -> 0으로 이동 시키는 것을 2번 진행했다.

 

즉, 위는 4를 맨 위로, 3을 두번째로 이동시키도록 했고

아래는 4를 맨위로 올린 후에, 3을 또 위로 올렸다.

 

그렇다면 뭘 언제 쓰라고??

-> 쓰고 싶은 것을 쓰면된다!! 뭐든 할 수는 있으니깐

하지만 만약 performBatchUpdates를 사용하지 않으면, 각각 애니메이팅이 일어날 때마다 tableView의 내부 값(indexPath나 Cell)등이 변경된다.

따라서 한번에 여러개의 애니메이션을 동작시키고 싶다면 Batch 방식을 사용하는 것이 나아 보인다.

 


또한 데이터를 추가/삭제 하고 난 뒤, 항상 tableView.reloadData() 하기 보다는 작업과 연관이 되는 Cell들만 Reload하는 것이 좋은 것이 분명하다!

tableView.reloadRows API가 있다고 하니 이 부분과 위의 부분들을 가지고 노는 걸 한번 해봐야겠다.

 

 

Animation

UIView.animate(...) 를 사용해서 애니메이팅을 구현할 수 있다.

 

animation(애니메이션) 사전적 의미

동작이나 모양이 조금씩 다른 많은 그림이나 인형을 한 장면씩 촬영하여 영사하였을 때에 화상이 연속하여 움직이는 것처럼 보이게 하는 것

어렸을 때, 책 가장자리에 조금조금씩 움직임을 표현하여 100p를 그린 뒤, 책을 빠르게 넘겨 촤르르르륵~ 하는 것이 애니메이션이었다..!

 

iOS에서의 Animation

Changes to several view properties can be animated—that is, changing the property creates an animation starting at the current value and ending at the new value that you specify. The following properties of the 
UIView
 class are animatable:

뷰의 여러 속성을 변경하여 애니메이팅을 할 수 있다. 즉, 속성을 변경하는 것은 현재 값에서 새로운 값으로의 애니메이션을 생성한다.

볼드체가 핵심이다..! 위의 애니메이션 정의만 생각하면 View를 조금조금씩 그려 촤라라라락하는 것처럼 해야하기 때문에, 직접 자잘한 움직임과 위치, 크기 등의 속성을 일일이 지정해줘야한다.

 

B.U.T.하지만 현재 값에서 새로운 값으로의 애니메이션을 생성한다!

 

애니메이팅할 수 있는 UIView의 속성은 다음과 같다.

 

먼저 Frame값을 변경해보자!!

이러한 화면이 있다고 하자!

 

UIView.animate(withDuration: 2.0) {
	self.playPauseButton.frame = CGRect(x: 0, y: 0, width: self.playPauseButton.frame.size.width, height: self.playPauseButton.frame.size.height)
}

위에서 얘기한대로 난 최종 상태값만을 지정해줬을 뿐인데!! 자연스럽게 이동한다!!!

 

다음으로 backgroundColor를 변경해보자

UIView.animate(withDuration: 2.0) {
	self.view.backgroundColor = .black
}

 

 

이 또한 흰색에서 검은색으로 변경한다고 지정했을 뿐인데! 흰색 -> 회색 -> 검은색 으로 자연스럽게 변환된다.!!

 


  • UIView.animate 를 사용한 애니메이팅은 자동으로 MainQueue(UIQueue)에서 동작한다
    • 따라서 따로 DispatchQueue.main.async를 사용하지 않아도 된다.
  • origin값을 변경하는 것이 아니라, Transform을 변경하는 것이다.
    • Transform.identity를 이용하여 원래값을 얻을 수 있다
  • 애니메이팅이 끝난 후, 동작하는 completionHandler가 존재한다.
    • Animation이 중간에 cancel되는 경우가 있어서 파라미터로 Bool 변수를 받는다.

 

+ Recent posts