티스토리 뷰

음악 플레이어에서 가장 중요한 코어 기능은 음악을 제어하는 기능이다. (플레이어 컨트롤러)

아래 사진은 이전 글에서 설명했던 앱의 구조인데, 뷰에 들어가는 공통 기능 중 ControlButtons 에 대한 설명을 해보려고 한다.

 

1. 개요

FLO 앱을 포함한 음악 플레이어 앱의 컨트롤 버튼을 생각 해보면, 가장 큰 특징은 어느 곳에서나 같은 역할을 한다는 점이다.

재생, 일시정지, 앞으로가기, 뒤로가기 등.. 여러 기능들이 있지만,

뷰가 다르다고 하여 다른 역할을 하지 않으며 음악을 컨트롤 하는 같은 역할을 한다.

 

특히 나는 음악을 재생하는 MusicPlayer 객체를 싱글톤으로 만들어서 앱의 어느곳에서든 항상 존재하도록 구현했다.

즉, 생성하는 버튼이 하는 일은 MusicPlayer 싱글톤 객체를 제어하는 동일한 기능을 할 것이다.

 

이러한 <같은 디자인><같은 기능>을 하는 버튼들을 뷰가 다르다고 하여 따로 구현하는 것은 비효율적이고 싫다. 코드가 중복된다.

이번 글에서는 내가 어떻게 버튼들을 관리하였는지 설명하고자 한다.

 

2. 버튼의 속성들을 enum으로 관리

먼저 구현할 버튼의 종류들을 살펴보자

  • 재생/일시정지
  • 이전 곡
  • 다음 곡
  • 반복
  • 재생 순서
  • 재생 목록

총 6개의 버튼을 enum으로 만들었다.

/// 버튼 모음
enum PlayerControlButtonType {
    /// 재생 버튼
    case play
    /// 이전곡 버튼
    case backward
    /// 다음곡 버튼
    case forward
    /// 반복 버튼
    case `repeat`
    /// 재생 순서 버튼
    case playOrder
    /// 재생목록 버튼
    case playList
}

 

그리고 각 버튼에 들어가는 이미지와 기능들을 다음과 같이 구현해주었다.

extension PlayerControlButtonType {
    /// 버튼 image의 config - 사이즈
    var imageConfig: UIImage.SymbolConfiguration {
        switch self {
        case .play:
            return UIImage.SymbolConfiguration(pointSize: 40, weight: .regular)
        case .backward, .forward, .playList:
            return UIImage.SymbolConfiguration(pointSize: 25, weight: .thin)
        case .repeat, .playOrder:
            return UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)
        }
    }
    
    /// 버튼의 이미지
    func getButtonImage(playStatus: PlayStatus) -> UIImage? {
        switch self {
        case .play:
            return UIImage(systemName: playStatus.iconImageName, withConfiguration: imageConfig)
        case .backward:
            return UIImage(systemName: "backward.end.fill", withConfiguration: imageConfig)
        case .forward:
            return UIImage(systemName: "forward.end.fill", withConfiguration: imageConfig)
        case .repeat:
            return UIImage(systemName: "repeat", withConfiguration: imageConfig)
        case .playOrder:
            return UIImage(systemName: "shuffle", withConfiguration: imageConfig)
        case .playList:
            return UIImage(systemName: "text.append", withConfiguration: imageConfig)
        }
    }
    
    func buttonAction(playStatus: PlayStatus) {
        switch self {
        case .play:
            playStatus.handler?()
        case .backward:
            print("뒤로가기")
        case .forward:
            print("앞으로 가기")
        case .repeat:
            print("반복")
        case .playOrder:
            print("PLAY ORDER")
        case .playList:
            print("PlayList")
        }
    }
}

enum PlayStatus {
    case playing
    case notPlaying
    
    var iconImageName: String {
        switch self {
        case .playing: return "pause.fill"
        case .notPlaying: return "play.fill"
        }
    }
    
    var handler: (() -> Void)? {
        switch self {
        case .playing:
            return MusicPlayer.shared.pause
        case .notPlaying:
            return MusicPlayer.shared.resume
        }
    }
}

위의 PlayerButtonType 중 Play의 경우 PlayStatus를 받아서 아이콘을 생성하고, 기능이 작동되도록 구현했다.

이렇게 enum을 만들어 버튼의 속성들을 정의해 두었지만, enum을 만든다고 해서 버튼을 만든 것은 아니다.

위 속성을 받아서 사용하는 커스텀 버튼 클래스를 만들어보자

 

3.  커스텀 버튼 클래스 생성

class PlayerControlButton: UIButton {
    //MARK: - Properties
    private var playButtonType: PlayerControlButtonType
    private var playStatus: PlayStatus {
        return MusicPlayer.shared.playStatus.value
    }
    private let disposeBag = DisposeBag()
    //MARK: - Lifecycle
    init(buttonType: PlayerControlButtonType) {
        self.playButtonType = buttonType
        super.init(frame: .zero)
        self.addTarget(self, action: #selector(didButtonTapped), for: .touchUpInside)
        self.tintColor = .white
        
        if buttonType == .play {
            // Play 버튼인 경우 bind에서 이미지 설정
            self.bindStatus()
        } else {
            // 아닌경우 이미지 설정
            self.setImage(buttonType.getButtonImage(playStatus: self.playStatus), for: .normal)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    //MARK: - Bind(Play 버튼인 경우만)
    private func bindStatus() {
        MusicPlayer.shared.playStatus
            .bind { [weak self] status in
                guard let self = self else { return }
                self.setImage(self.playButtonType.getButtonImage(playStatus: status), for: .normal)
            }
            .disposed(by: disposeBag)
    }
    
    //MARK: - Methods
    @objc private func didButtonTapped() {
        self.playButtonType.buttonAction(playStatus: self.playStatus)
    }
}

코드는 간단하다.

UIButton을 상속받는 PlayerControlButton은 생성자 함수에서 앞에 만들었던 PlayerControlButtonType 을 파라미터로 받는다.

 

버튼 이미지 설정

  • Play 버튼 이외의 버튼들은 .setImage 를 통해 PlayerControlButtonType에서 정의한 이미지를 세팅한다.
  • Play 버튼 일 경우 MusicPlayer의 PlayStatus에 따라서 이미지와 기능이 바뀌어야 한다. (재생/일시정지)
  • 따라서, bindStatus() 함수를 호출하게 되는데, 그곳에서는 MusicPlayer의 playStatus를 구독하여서 해당 status에 맞는 이미지를 설정하도록 구현했다.
  • 그리고 구독은 play인 경우 buttonType이 play인 경우에만 실행되도록 했다.

 

버튼 액션 설정

  • 버튼의 Selector로 구현한 didButtonTapped() 함수에서는 아까 설정한 PlayButtonType의 buttonAction() 함수가 호출되도록 했다.
  • Play/Pause 액션의 경우 파라미터로 들어간 playStatus 에서 MusicPlayer 싱글톤 객체의 컨트롤을 구현해두었기 때문에, 그 부분이 호출되도록 했다.

 

4.  커스텀 버튼 인스턴스 생성 / 사용하기

이제 각 타입별로 버튼의 인스턴스를 생성해보자

extension PlayerControlButtonType {
    /// 버튼 인스턴스
    var getButton: UIButton {
        return PlayerControlButton(buttonType: self)
    }
}

아까 만들어둔 enum PlayerControlButtonType 의 getButton 연산 프로퍼티를 생성하고,

자기 자신을 생성자 함수에 넣어 PlayerControlButton 인스턴스를 리턴하도록 구현했다.

여기까지 구현했다면, PlayerControlButtonType.play.getButton 으로 간단하게 인스턴스를 얻을 수 있다.

 

그럼 뷰 컨트롤러에서 사용해 뷰를 구현해보자

앞에서 모든 기능을 구현했기 때문에 뷰에서는 정말 간단하게 사용할 수 있다.

class LyricsController: UIViewController {
    //MARK: - Properties
    private let controlPanel: [PlayerControlButtonType] = [.repeat, .backward, .play, .forward, .playOrder, .playList]
    
    ...
}

만들 버튼들을 PlayerControlButtonType 을 배열에 넣었다. 그리고 아까 만들었던 getButton 프로퍼티를 활용하면 버튼 인스턴스를 받을 수 있다. 

let buttonStack = self.controlPanel.map { $0.getButton }
let stackView = UIStackView(arrangedSubviews: buttonStack)
stackView.axis = .horizontal
stackView.distribution = .fillEqually

위와 같이 버튼을 스택뷰로 만들어 제약조건을 잡고 화면에 보여주면 의도했던 대로 잘 동작한다

 

5. 결론

이렇게 공통된 이미지와 같은 기능을 하는 버튼을 모듈식으로 만들어 구현했다.

처음 설계했던대로 잘 동작하였다.

구현한 버튼들의 동작을 확인해보자

 

내가 생각했을 때의 장점은 다음과 같다.

  • 버튼의 속성과 하는 일은 PlayerControlButtonType, PlayerControlButton 에서 모두 정의하고 있으므로 유지보수가 용이하고, 구현에 필요한 코드가 줄어든다.
  • 앱이 확장되어 저 버튼들이 필요한 다른 뷰에서도 간단하게 사용 가능하다.

 

소스코드: https://github.com/jisu15-kim/FloMusicPlayer

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함