탭바를 아래로 내리고 위로 올리는 애니메이션을 적용하고 싶었다...

 

그래서 tabBar.frame.size.height = 0 를 animate코드에 넣었는데 원하는 동작이 아니었다...

알아보던 중 size가 아닌 origin.y를 내리고 올리는 방식은 정상동작 한다더라...

if let frame = tabBarController?.tabBar.frame {
	let factor: CGFloat = true ? 1 : -1
	let y = frame.origin.y + (frame.size.height * factor)
	UIView.animate(withDuration: 1.0, animations: {
		self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: y, width: frame.width, height: frame.height)
	})
}

 

원인은 숙제로 나중에 알아봄!

이전 포스팅에서 간략하게 iOS에서의 애니메이션을 알아보았다.

 

이번에는 좀 더 자세히 알아볼 것이다.

 

View를 구성하는 방법

  1. Frame을 이용
    1. code
  2. AutoLayout을 이용
    1. xib
    2. storyboard

크게 이 두가지 방법을 통해 View를 그리게 된다.

 

Frame-Based Layout

각 View들의 frame을 programmatically하게 설정하여 배치

frame에는 상위뷰(Super View)의 좌표계에 대한 자신의 위치 = origin

상위뷰(Super View)의 좌표계에 대한 자신의 크기 = size

를 가진다.

 

(bounds라는 개념도 존재하는데... frame vs bounds는 이후에 포스팅하는 걸로

중요한 점은 frame은 상위뷰의 좌표계, bounds는 자신의 좌표계라는 것이다.)

 

쨋든 이어서

frame의 정보를 일일이 코드로 작성해야 한다.

-> 일일이라고 적어놓으니깐 불편해 보이는데 좋게 말하자면, AutoLayout의 그 많은 Constraint들을 일일이 작성하지 않아도 된다.

이렇게 말하면 좀 편해보이나?

하지만, 가장 중요한 점 = 아이폰은 지금 12까지 나왔다!

각 아이폰 기기 별 크기는 다르다. -> 개발자가 모든 화면별로 사이즈를 대응해서 코드로 작성해줘야 한다. -> 굉장히 굉장하네

 

frame은 상위뷰 좌표계에서 그려지고 동작한다. 만약, View -> View -> View 같은 nestedView Depth가 깊어질 경우

개발자의 머리는 남아나질 않을 것이다. -> 모두 계산해줘야 하니까

 

 

AutoLayout

frame과는 다른 개념이다. frame체계를 머리속에서 지워보자

 

AutoLayout은 view 사이의 관계를 맺음으로써 화면을 그린다.

Constraint을 기반으로 각 View들의 크기와 위치를 계산한다.

 

이전 포스팅에서 간단하게 작성된 것을 보면 될 것 같다.

 

 

Animate

애니메이션 글인데 왜 frame과 AutoLayout을 적었냐 하면... 사용법에 차이가 있어서 설명했다...(라고 할뻔)

이전 포스팅에 있는 예제를 살펴보자

 

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)
}

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

두 애니메이션 모두 정상적으로 돌아간다.

그렇다면 frame이 아닌 AutoLayout으로 이뤄진 화면에 적용해보자!

View의 높이를 0으로 만들것이다!

 

 

// view1이라는 녹색 View를 붙였다.
func setView() {
	view.addSubview(view1)
	view1.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
	view1.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
	view1.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
	view1.heightAnchor.constraint(equalToConstant: 60).isActive = true
}

 

자 이제 애니메이션을 적용해보자

 

UIView.animate(withDuration: 1.0) {
	self.view1.frame.size.height = 0
}

 

엥? 난 분명 높이를 0으로 줬는데 왜 오히려 커졌다가 줄어들어?

위에서 view1을 UIView(frame: )으로 생성한 것이 아닌 AutoLayout으로 뷰를 그렸기 때문에, 애니메이션 방법도 달라야 한다.

 

높이 Constraint를 변경하기 위해 프로퍼티를 생성한 후, 참조시키자.

 

var heightConstraint = NSLayoutConstraint()
....
func setView() {
	view.addSubview(view1)
	view1.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
	view1.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
	view1.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
	heightConstraint = view1.heightAnchor.constraint(equalToConstant: 60)
	heightConstraint.isActive = true
}

 

UIView.animate(withDuration: 1.0) {
	self.heightConstraint.constant = 0
}

그리고 실행 시켜보자.

 

0으로 되긴 했네... 근데 애니메이팅이 아니라 그냥 뿅??

 

frame이나 alpha, backgroundColor 는 Animatable한 built-in 속성이기 때문에 애니메이팅이 동작한다.

하지만 Constraint는 Animatable한 built-in 속성이 아니기 때문에, layoutIfNeeded()라는 메소드의 호출이 필요하다.

layoutIfNeeded()는 또 이전 포스팅에 있다.

 

호출이 필요한 이유는 Constraint의 변경이 실제로 View에 적용되기 위해서는 일련의 순서가 필요하기 때문이다.

  1. 변경될 값에 맞춰 시스템이 해당 Constraint를 포함하여 연관있는 Constraint들의 모든 값들을 재계산
    1. 앞에서 말했 듯, Constraint는 view들의 관계이기 때문에, 하나의 View가 변경되면 다른 View들도 그에 맞춰 변경되기 때문
  2. layout 엔진이 재계산된 Constraint에 맞춰 모든 View들의 frame들의 값을 재계산하고 배치한다.
  3. 해당 frame들을 화면에 그린다.

그럼 그렇게 다시 해보자!

heightConstraint.constant = 0
UIView.animate(withDuration: 1.0) {
	self.view1.layoutIfNeeded()
}

엥? view1.layoutIfNeeded()까지 했는데 왜 안돼...?

(개인적인 생각으로는 view1의 Constraint들은 대부분 self.view와 연관되어 있다. 그러한 이유로 안되지 싶다...)

 

그럼 코드를 바꿔서 진행해보자

heightConstraint.constant = 0
UIView.animate(withDuration: 1.0) {
	self.view.layoutIfNeeded()
}

정상적으로 원하는 대로 동작한다..!

 


정리

  1. frame방식과 AutoLayout을 통해 View를 그릴 수 있다.
  2. frame과 AutoLayout 각각 다른 코드진행으로 animate할 수 있다.
    1. frame과 alpha 등은 Animatable한 built-in 속성이기 때문에, layoutIfNeeded()가 필요없다
    2. AutoLayout은 Animatable한 built-in 속성이 아니기 때문에, layoutIfNeeded()가 필요하다
  3. animation을 적용한 view에서 layoutIfNeeded()를 호출하는 것이 아니라, 해당 view와 Constraint로 관련되어있는 SuperView에서 호출해야 한다.

 

 

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

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()는 오디오를 재생할 때까지 미루는 것이 좋다고 한다.

 

+ Recent posts