0. 시작

한참동안 또 안건드리다가 다시 잡게되었다.

이전에 잠금화면에서 재생화면으로 뜨는거와 mvvm으로 나눠둔것까지 했었다. 기능적으론 딱히 더 필요한게 재생바에서 재생시간옴기는거려나 그럼 이거 추가하는김에 SwiftUI를 공부도 하려니 uikit부분을 swiftui로 변경할거다.

SwiftUI 뷰만 만들어봐야 별로 다를것없어보이니 기존과 꽤나 다른점인 @Published 이게 눈에 보여서 @EnviromentObject이걸 뷰모델에 넣어서 뷰와 연결해볼거다

@EnviromentObject는 컨테이너시스템에서 시스템 시작할때 환경변수파일 넣어주듯이 넣는 개념이라 이해하고있다

이 객체로 선언된건 바로 뽑아와서 쓰고 Published 태그가 붙은 변수들은 변경될때마다 이벤트를 발생시켜 구독중인 애들이 새로 그려진다는듯하다

이것만들으면 그냥 notification을 변수의 set 항목에 지정해서 쓴거아닌가 싶은데 뭐 아무튼 설명이 그러니 그냥 쓰려고한다

 

1. 기존 프로젝트에서 어떻게 바꿀까?

기존 파일 구성이

ControllViewController.swift

MusicInfo.swift

MusicPlayer.swift

NewMainVC.swift

PlayList.swift

PlayListCell.swift

였는데

NewMainVC가 겉으로 

여기에 테이블을 넣고 테이블셀은 PlayListCell로 지정해서 모양을 잡아줬고

테이블 아래로는 ControllViewController를 집어넣어서 위에는 파일리스트 아래는 재생버튼으로 뷰를 마무리 하고

실 플레이 기능은 MusicPlayer와 음악데이터값은 PlayList에 나눠서 구성했었다.

기능별 분리를 해놔서 swiftUI로 변경은 어렵지 않을거라 생각했건만 

실 기능과 데이터가 있는 MusicPlayer와 PlayList 클래스를 ObservableObject를 달아줘서 밖으로 쓸 변수를 @Pbulished를 해줬다

그리고 이건 망했다 왜 망했는지는 뷰를 그리고 나서 이야기하자

 

뷰는 일단 따로 ai로 그려진걸 그려볼까했는데 막상 하려니 내가 원하지 않는 기능들이 덕지덕지 붙은 그림을 줘서 그냥 포기.

기존과 비슷하게 그러나 swiftUI기본 디자인이 워낙 괜찮아서 같은 위치에 같은 기능이나 기존보다 살짝 더 디자인적으로 나아졌다

우선 가장 겉은 부분은

MainScreen 이라는 이름으로 뷰를 그려줬다 기존 방식그대로 위에는 리스트, 아래는 컨트롤바

VStack(spacing: 0) {
    PlayListView()
    ControllBarView()
}

기본 스페이싱을 없애기위애 따로 spacing 0을 넣어줬다.

 

PlayListView는 뭐 별다를거 없는데

  var body: some View {
    List(player.musicData) { item in
      Text(item.title)
    }
    .listStyle(.automatic)
    .onAppear(perform: {
      player.loadList()
    })
  }

셀지정이나 그런것도 없이 바로 Text를 이용해 뿌려주면 끝! 너무 간단해서 이걸 굳이 나눴어야 했나 싶을 정도로 끝이 났고

생각해보니 원본도 테이블뷰를 메인에 넣었던거같아서 실수했다. 

 

아래는 컨트롤바로 기존과 다른점이라곤 버튼에 배경을 넣어줬다

원래는 자리차지가 어느정도 되나 집어넣은 테스트 색상이였는데 보다보니 괜찮아보여 그냥 냅뒀다

  var body: some View {
    VStack(
      alignment: .leading,
      spacing: 4,
      content: {
        Button(action: {
          player.loadList()
        }, label: {
          Text("내부파일 재로드")
        })
        .padding(.horizontal)
        .foregroundColor(.white)
        .background(Color.blue)
        .cornerRadius(10)
        .font(Font.title3)
        
        HStack(content: {
          Text("\(minuteString(to: player.currentTime))")
          Slider(value: $player.currentTime, in: 0...player.duration) { isEditing in
            if !isEditing {
              player.seek(to: player.currentTime)
            }
          }
          Text("\(minuteString(to: player.duration))")
        })
        .padding(.horizontal, 8)
        
        HStack(content: {
          Spacer()
          
          ControllButton(text: "❮") {
            _ = player.previousMusic()
          }
          ControllButton(text: player.isPlaying ? "❚❚" : "▶︎") {
            _ = player.play()
          }
//          .disabled(true)
          ControllButton(text: "❯") {
            _ = player.nextMusic()
          }
          
          Spacer()
        })// HStack
        
        Color.clear
          .frame(height: 8)
      })
  }
  
  struct ControllButton: View {
  let text: String
  let action: () -> Void
  
  var body: some View {
    Button(action: action) {
      Text(text)
        .font(.title)
        .frame(width: 50)
        .foregroundColor(.white)
        .background(Color.blue)
        .cornerRadius(10)
    }
  }
}

뷰 파일을 이렇게 만들려고했으면 기존엔 앵커로 엄청 줄줄이 써댔거나 snapkit으로 그나마 짧게 잡아줬을텐데

이런거 보면 정말 간단해졌다.

 

 

2. 뷰를 만들어주고 연결해주려니 에러가 났다.

1의 뷰랑 연결시켜야하는 기존의  MusicPlayer와 PlayList 클래스에대한 문제는...

MusicPlayer에서 PlayList를 참조한다는점에서 문제가 발생했다

 

--

ControllBarView.swift:27:36: error: cannot find type 'PlayList' in scope

@EnvironmentObject var playList: PlayList

--

둘다 ObservableObject로 만들어주고 처음화면의 뷰에다가 

.environmentObject(musicPlayer)

.environmentObject(playList)

이렇게 두개를 주입해줬는데 musicPlayer에서 playlist를 찾지못한다는 에러가 발생 했다 처음하는 swiftUI라 뭔가 잘못했구나 싶어 gpt에게 열심히 물어봤지만, 무료분량이 끝날때까지 이놈은 이상한 소리만하고 결국 고쳐지지 못했었다

그리고 누군가 올린 블로그글에서 문제를 찾았는데....

뷰모델에선 뷰모델을 부르지 못한다고한다

단순히 notifiation이라고 생각하고 있던 내가 아 잘못이해했구나 한걸 깨달게되는 사건이였다

싱글톤에서 벗어나 바인드도 하고하는 좀 rx스타일같은걸 배우고싶었는데 이제와서 다시 싱글톤으로 하기엔 미묘하다

 

결국 그렇게 난 이 상태를 포기하고 MusicPlayer와 PlayList 클래스를 합쳤다

주입해주던 클래스도 하나로 변경되서

.environmentObject(player)

로 깔끔해졌다.

 

뭐 결국 그냥저냥.... 그렇게 되었다

아 재생바는 slider로 연결해서 값 변경이 완료되면 

  func seek(to time: TimeInterval) {
    avPlayer.currentTime = time
    currentTime = time
  }

이 함수로 변경해주도록 해서 마무리.

뭐 결국 그렇게 테스트해보고 잘됐다.

역시 기본설정들이 다크모드용으로 따로 스타일 지정안해줘도 보기좋게 나온다

리스트도 딱히 만져준게 없는데도 나름 예쁘게 잘나왔고 슬라이더도 잘동작하니 만족

여기서 더 만질일이 있으련지는 모르겠다

나중에 뭔가 추가한다면 리스트에서 선택해서 재생하도록하는거?

그런데 나는 그냥 긴 파일을 다운받아서 재생해놓기에 쓸일이 있으려나

아니면 웹주소로 재생되게하는거?

이건 초기 컨셉이였던거같은데 생각나면 해야지

 

변경된 전체 코드는 역시 내 깃에 ~ 

https://github.com/wiwi-git/proj_ypl/

 

GitHub - wiwi-git/proj_ypl: ios에서 간단하게 쓸 백그라운드 플레이어가 필요하다

ios에서 간단하게 쓸 백그라운드 플레이어가 필요하다. Contribute to wiwi-git/proj_ypl development by creating an account on GitHub.

github.com

 

3. ps

다 쓰고 나니 생각났는데

기존 uikit으로 만든 프로젝트이기에 이 프로젝트에는 Appdelegate와 SceneDelegate파일이 있는 프로젝트다

거기다 추가로 스토리보드 기반이 아니기에 SceneDelegate에서 루트 vc를 설정해준 프로젝트다

 

이걸 시작지점부분을 swfitUI파일과 연결해줘야하는데

swiftUI는 struct이고 view이기에 viewcontroller가 없다 그래서 추가로 따로 만들어줘야한다 이렇게

let hostingController = UIHostingController(rootView: MainScreen())
window?.rootViewController = hostingController

그렇게 만들어주고 기존에 하던데로 루트를 만들어준거로 넣어주면 끝!

 

참고로 mainScreen에는 App 프로토콜뷰가 없다 App은 SwiftUI기반 프로젝트를 생성하면 나오는애

 

반응형

model, view, viewmodel 이렇게 세가지로 나누어서 유지 보수 및 테스트에 적합하게 구성한 패턴을 mvvm이라 부른다

이 방식은 기존 uikit을 이용하는 swift에서 소개라 해야하나 권장해야한다해야하나 그런 cocoaMVC방식

- model, view, controller 세부분으로 나누던 mvc방식에서 View-Controller, model 두가지로 나누는 방식 

에서 swiftUI를 나오면서 뷰와 컨트롤러를 나누기 쉽게되어 많이 쓰이고있다

 

솔직히 무슨 패턴이니 하면서 하는걸 대부분 혼자 프로젝트 생성부터 배포까지 해왔던 나에겐 크게 다가오지 않는 방식이다

이런거 없이 개발해오던 내 느낌으로는 어떤 특정 스킬을 전체적인 규칙으로 특화해둔 느낌이라 뭔가 예의차리는 느낌이다

 

감상은 그만 말하고 다시 mvvm에 대해 이야기하자

ui 부분과 실 서비스 컨트롤부분 그리고 데이터 부분을 나눈방식이라 이해가 가는데 영 감이 안잡힌다 그래서 일단 해봤지만 잘 분리가 안됐을수도 있다.

 

아 참고로 이렇게 코드를 분리한 이유는 맨위에서 말했듯이 SwiftUI때문이다 이걸 공부하고싶은김에 이 프로젝트의 ui를 uikit에서 swiftui로 변경하고싶어 코드를 세부분으로 변경했다.

라곤 했지만 솔직히 기존 프로젝트에서 크게 다르지않다. 메인vc에서 뮤직플레이어로 서비스부분을 분할했다.

 

 

나눈 부분의 이야기를 하자

(일단 변명을 미리 하자면 내가 이해하기로는 model은 데이터구조, view는 ui코드, viewmodel은 비지니스코드로 이해했다)

model 은 다를 음악 파일의 데이터가 될 MusicInfo

view는 기존 NewMainVC, PlayListCell

viewmodel은 PlayList, MusicPlayer

이렇게 분리되었다.

뭐 추가된부분은 musicplayer부분만이지만

 

view부분에서 연결된 부분은

@objc func reloadButtonAction() {
    PlayList.shared.loadList()
    self.playListView.reloadData()
  }
  
  @objc func playButtonAction(){
    _ = player.play()
    setPlayIcon(isPlay: player.isPlaying)
  }
  
  @objc func preButtonAction() {
    _ = player.previousMusic()
    setPlayIcon(isPlay: player.isPlaying)
  }
  
  @objc func nextButtonAction() {
    _ = player.nextMusic()
    setPlayIcon(isPlay: player.isPlaying)
  }

기존 같이 있던 플레이어 기능을 전부 MusicPlayer.swift 파일로 빼서 이렇게 한단계 거치게됐다

참고로 찾아보니 테이블뷰에 Datasource 부분도 따로 빼서 연결해둔사람이 있었는데

cellForRowAt 부분에 셀의 ui변경코드를 넣어둬서 따로빼면 오히려 더 알아보기 힘들어질것같아서 냅뒀다

플레이어부분은 아래처럼 한번 감싸서 한번 생성되어 사용되도록해서 여러 부분에서 불러도 지장없이 해놨다

class MusicPlayer : NSObject {
  static var shared: MusicPlayer = .init()
  
  var avPlayer: AVAudioPlayer = .init()
  var playList: PlayList = .shared
  var paused: Bool = false
  var isPlaying: Bool {
    get {
      return self.avPlayer.isPlaying
    }
  }
  
  private override init() {
    super.init()
    initPlayer()
    remoteCommandCenterSetting()
  }
  
  private func initPlayer() {
    let audioSession = AVAudioSession.sharedInstance()
    do {
      /// 이유를 모르겠다 옵션즈에 값을 넣으면 락스크린에 컨트롤바가 안생김....
//      try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
      try audioSession.setCategory(.playback, mode: .default, options: [])
    } catch let error as NSError {
      print("audioSession 설정 오류 : \(error.localizedDescription)")
    }
  }
  
  ~~~

 

전체 부분을 보고싶으신분은 github을 봐달라.

https://github.com/wiwi-git/proj_ypl

 

GitHub - wiwi-git/proj_ypl: ios에서 간단하게 쓸 백그라운드 플레이어가 필요하다

ios에서 간단하게 쓸 백그라운드 플레이어가 필요하다. Contribute to wiwi-git/proj_ypl development by creating an account on GitHub.

github.com

 

이거로 3편에서 계획한 5단계에서 큐플레이어로 대체하는것을 제외하고 끝났다

막상하려니 고쳐야하는 부분이 많아보여서 그냥 이부분은 넘어가도록했다 기능적인 문제에 별거없었고...

 

다음은 swiftUI로 변경해보는김에 디자인을 좀 꾸며볼 생각이다

솔직히 너무 없어보여서;;;;

원래는 나 혼자 그냥 공부겸 만들거라 기능만 있으면 되겠지 싶었는데 이렇게 블로그에 글을쓰다보니 조금 부끄러워졌다

마침 디자인해주는 공짜ai서비스에 대해 듣기도했고 이걸 이용해 볼 생각이다

 

 

반응형

 

폰으로 음악을 자주 듣지않고있기에 간단히 만든 앱을 사용하여 가끔 음악을 듣는다

유튜브영상을 URL을 입력하면 mp3파일로 만들어 다운로드하게 해주는 사이트에서 파일을 받아 재생하는 용도이다

솔직히 이러면 그냥 기본 파일앱에 연동되어있는걸로 재생하는게 편한데 어쩌다 3편글까지 왔는지 기억이 가물가물...

아마 공부를 겸해서 그런거겠거늘한다 

아무튼 기본앱에는 있는 기능은 잠금화면에서의 컨트롤바가 내 앱에는 없다

그래서 이번엔 이걸 추가하려고하였으나....

 

며칠 고생했으나 문제를 찾지 못했고 그렇게 그냥 묵혀두다 오늘 다시 시작했다

아마 설정문제이겠거늘 싶긴한데 예제로 구한 앱과 다른 설정부분을 찾지 못했고 나는 이참에 아예 소스를 다 들어내고 처음부터 배치를 다시할까 한다

우선 관련된 클래스는

MPRemoteCommandCenter

https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter

 

MPRemoteCommandCenter | Apple Developer Documentation

An object that responds to remote control events sent by external accessories and system controls.

developer.apple.com

그래고 메서드는

beginReceivingRemoteControlEvents()

 

 

 

 

https://developer.apple.com/documentation/uikit/uiapplication/1623126-beginreceivingremotecontrolevent

 

beginReceivingRemoteControlEvents() | Apple Developer Documentation

Tells the app to begin receiving remote-control events.

developer.apple.com

 

 

추가로

공식예제는 아래의 링크이다.

https://developer.apple.com/documentation/mediaplayer/becoming_a_now_playable_app

 

Becoming a now playable app | Apple Developer Documentation

Ensure your app is eligible to become the Now Playing app by adopting best practices for providing Now Playing info and registering for remote command center actions.

developer.apple.com

 

그런데 솔직히 공식 예제는 분리가 너무 많이 되어있어서 읽기 귀찮다

 

뭐 아무튼 그래서 우선 모든 코드들을 동작하지 않게 처리하고

아래와 같은 순서로 앱의 리빌드할꺼다

 

1. 백그라운드 오디오 재생에 필요한 권한 처리 info.plist

2. 플레이어 생성 -> 기존 avplyaer에서 avQueuePlayer로 대체

3. MPRemoteCommandCenter 설정 및 설정

4. ui코드 재위치

5. 각 이벤트 연결 및 테스트

 

 

 

 

 

- 계획만 짜두고 한동안 잡지않다가 다시 시작

기존 코드들을 죄다 old폴더에 넣고 새로운 vc에서 통합하여 사용하여 문제되는 라인을 찾았다

전부 다 통합하여 재 구축해도 되지않아서 당황하다가 얼떨결에 찾은거라 이유를 모르겠다 누가좀 알려줬으면 좋겠네

문제되는 라인은 바로

//      try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers])
      try audioSession.setCategory(.playback, mode: .default, options: [])

카테고리 설정하는 부분

타 앱 소리가 나도 같이 들리도록 옵션을 추가해줬었는데

이 설정을 하다가 깜빡 옵션을 안넣어줬었는데 락스크린에 컨트롤바가 생겼다.

너무 어처구니없는 상황에 웃음만 나온다

 

일단 이 포스트가 아마도 락스크린의 컨트롤바를 설정하는거라 해당 부분 소스를 올리자면

  func remoteCommandCenterSetting() {
    // remote control event 받기 시작
    UIApplication.shared.beginReceivingRemoteControlEvents()
    let center = MPRemoteCommandCenter.shared()
    center.playCommand.removeTarget(nil)
    center.pauseCommand.removeTarget(nil)
    
    // 제어 센터 재생버튼 누르면 발생할 이벤트를 정의합니다.
    center.playCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
      self.avPlayer.play()
      MPNowPlayingInfoCenter.default()
        .nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: self.avPlayer.currentTime)
      // 재생 할 땐 now playing item의 rate를 1로 설정하여 시간이 흐르도록 합니다.
      MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 1
      return .success
    }
    
    // 제어 센터 pause 버튼 누르면 발생할 이벤트를 정의합니다.
    center.pauseCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in
      self.avPlayer.pause()
      MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: self.avPlayer.currentTime)
      // 일시정지 할 땐 now playing item의 rate를 0으로 설정하여 시간이 흐르지 않도록 합니다.
      MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = 0
      return .success
    }
    
    center.nextTrackCommand.addTarget { event in
      let nextResult = self.nextAction()
      if !nextResult {
        return .noSuchContent
      }
      
      return .success
    }
    
    center.previousTrackCommand.addTarget { event in
      _ = self.previousAction()
      
      return .success
    }
    
    center.playCommand.isEnabled = true
    center.pauseCommand.isEnabled = true
  }
  
  func remoteCommandInfoCenterSetting() {
    let center = MPNowPlayingInfoCenter.default()
    var nowPlayingInfo = center.nowPlayingInfo ?? [String: Any]()
    
    guard let currentMusic: MusicInfo = playList.currentMusic else {
      print("리스트에 곡이 없음")
      return
    }
    
    nowPlayingInfo[MPMediaItemPropertyTitle] = currentMusic.title
    nowPlayingInfo[MPMediaItemPropertyArtist] = currentMusic.artist
    
    if let albumCoverPage = currentMusic.artworkImage {
      nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: albumCoverPage.size, requestHandler: { size in
        return albumCoverPage
      })
    }
    
    // 콘텐츠 총 길이
    nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = self.avPlayer.duration
    // 콘텐츠 재생 시간에 따른 progressBar 초기화
    nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1
    // 콘텐츠 현재 재생시간
    nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: self.avPlayer.currentTime)
    
    center.nowPlayingInfo = nowPlayingInfo
  }

이곳저곳에서 퍼오면서 테스트하고 기우고 이래서 출처가 기억이 안남.

 

전체 소스는 내 깃에 있다.

https://github.com/wiwi-git/proj_ypl

 

GitHub - wiwi-git/proj_ypl: ios에서 간단하게 쓸 백그라운드 플레이어가 필요하다

ios에서 간단하게 쓸 백그라운드 플레이어가 필요하다. Contribute to wiwi-git/proj_ypl development by creating an account on GitHub.

github.com

 

todo로 잡아뒀던 기능들이 몇개 있는데 이건 그냥 포기하고 이후에 다시 글을 쓰게된다면 통합시켜놨던걸 mvvm으로 분리시키고

uikit으로 작성된 ui를 swiftui로 변경해볼까 한다

swiftui는 그냥 몇번 훑어보고 말아서 이참에 공부해야지

반응형

https://wiwi-pe.tistory.com/254

 

background audio play - 1

유튜브 프리미엄 해지후 몇 달이 흘르고 흘러 밖으로 돌아다닐일이 생겼으나 나의 유튜브 뮤직은 더이상 사용할 수 없었다.그렇다고 다시 결제하자니 초반 유튜브 레드 유저에게 약속했던 변하

wiwi-pe.tistory.com

background audio play - 1 에 이어 글을 쓴다.

 

그땐 아이폰이 무음상태일때 재생이 되지 않는 문제가 있어서 고친거로 기억이 나는데

그후에 오디오 세션에 대한 글을 읽다보니 이게 앱사용중엔 항상 세션을 사용상태로 두는게 옳은건가 의문이 들어 위치를 변경했다

또 겸사겸사 AvAudioPlayer 변수를 재사용할 수 있도록 변경하려다 보니 일시정지 상태를 구분하기가 미묘해져 따로 변수로 관리해주려고 코드를 좀 분할했다

 

아래 대로 세션 사용은 플레이 함수 하나로 통일한곳이 실행될때마다 엑티브 상태를 요청하게 했고

요청이 에러 없이 된다면 바로 paused를 false로해서 일시정지중이든 처음시작이든 항상 false가 기본이 되도록 하였다

  private func play(url: URL?) {
    defer {
      contBarView?.setPlayIcon(isPlay: avPlayer.isPlaying)
      NotificationCenter.default.post(name: .changedContInfo, object: nil)
    }
    
    do {
      try AVAudioSession.sharedInstance().setActive(true)
      paused = false
      
      if avPlayer.isPlaying == false, paused {
        avPlayer.play()
        return
      }
      
      guard url != nil else {
        return
      }
      // TODO: avplayer를 재사용하려고했더니 딱히 방법이없다 AVQueuePlayer로 변경해야할거같다 그냥 처음부터 이거만 있지 왜 오디오플레이어가 따로 존재할까
      avPlayer = try AVAudioPlayer(contentsOf: url!)
      avPlayer.delegate = self
      avPlayer.prepareToPlay()
      avPlayer.play()
      
    } catch  {
      NSLog(error.localizedDescription)
    }
  }

중간에 보면 항상 player의 delegate를 이클래스에서 한다고 계속 지정해주고있는데....

매우 이상해보인다 그런데 다음곡으로 넘어가기위해

audioPlayerDidFinishPlaying 라는 AVAudioPlayerDelegate에 있는 녀석을 사용하기위해서는 avPlayer가 초기화가 되버리면 애를 사용한다는 설정이 날라가버려서 어쩔 수 없었다.

곡을 선택할때 항상 다시 생성해주는 이유는 url을 변경하는 방법이 보이지 않아서 그랬다.

 

이러한 사태를 해결 하기위해 열심히 검색을해 보았으나.... avqueuePlayer라는걸로 사용해야하는것같으나 

개인적으로만 사용하고 말 소스라 굳이 변경해야하나 싶어 매 곡마다 avPlayer를 새로 만들어주고 delegate설정도 해주는걸로 땜빵

 

아 재생 상태 변경후에 Notification으로 알려서 타 클래스여도 옵저버로 알 수 있도록한건 꼼수다

굳이 이렇게 할 필요없이 delegate를 만들어서 전달해줘도 될듯싶으나 구조를 변경하기 귀찮아서 그렇게 했다

 

반응형

유튜브 프리미엄 해지후 몇 달이 흘르고 흘러 밖으로 돌아다닐일이 생겼으나 나의 유튜브 뮤직은 더이상 사용할 수 없었다.

그렇다고 다시 결제하자니 초반 유튜브 레드 유저에게 약속했던 변하지않는 가격으로~~ 라는 광고 문구를 속인게 괴심하다 생각해 도저히 못하겠고

그냥 파일로 다운받아서 돌리자 생각했다

해서 백그라운드 재생 및 플레이 리스트가 있는게 필요한데 오랜만에 네이티브 만지는김에 필요한것만 만들어보려고 했고 깃허브에 업로드시켰다 

아래는 일부분...

 

우선 음악을 담을 폴더 내부를 읽어서 url을 저장하는 부분...

    
    do {
      let directoryContents: [String] = try fileManager.contentsOfDirectory(atPath: filePath.path)
      print(directoryContents)
      if directoryContents.isEmpty {
        urls = []
        return
      }
      
      var _urls: [URL] = []
      
      for item in directoryContents {
        let fileURL = filePath.appendingPathComponent(item)

        if fileURL.pathExtension != "mp3" {
          continue
        }
        _urls.append(fileURL)
      }
      self.urls = _urls
    } catch  {
      NSLog(error.localizedDescription)
    }

다른앱이였다면 여러가지 확장자를 지원했겠지만 난 오로지 mp3파일만을 골랐다

 

그렇게 url을 싱글톤 클래스로 관리하고 따로 컨트롤바에서 플레이해주기로 결정

아래는 url을 플레이~

  private func playUrl(url: URL) {
    do {
      avPlayer = try AVAudioPlayer(contentsOf: url)
      avPlayer?.prepareToPlay()
      avPlayer?.play()
    } catch  {
      NSLog(error.localizedDescription)
    }
  }

 

테스트로 돌려봤는데 시뮬레이터에서는 재생이 되나 왠지 기기에서는 들리지않는다.

열심히 검색해보니 오디오 세션으로 플레이백 모드로 해줘야한다고 한다

해서 appdelegate에 해당 부분을 넣어줬다

try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers])
try AVAudioSession.sharedInstance().setActive(true)

해당 부분의 설명은 

https://wlaxhrl.tistory.com/92 를 봤다 감사할따름

일단 위글의 일부분을 가져와봤다

  • mixWithOthers: 다른 앱의 소리와 우리 앱 소리가 함께 들릴 수 있게 하는 옵션.
  • duckOthers: mixWithOthers 를 기본으로 채택하면서 +우리 앱에서 소리가 날 때는 다른 앱 소리를 살짝 죽이는 옵션.
  • interruptSpokenAudioAndMixWithOthers: mixWithOthers를 기본으로 채택하면서 + 다른 앱에서 spokenAudio 가 출력될 때에만 interrupt 걸고 우리 앱이 소리를 독차지 (*라디오, 팟캐스트 등을 생각하면 됨)

뭔가 많으니 한번 읽어보면 좋을거같음

 

/---

읽다보니 인터럽트 부분도 생각해줘야하고

아예 잊고있었던 곡하나가 끝나면 다음곡으로 바로 재생되는것도 있고...

이 두부분은 우선 나중에 해줘야겠다

 

반응형

드디어 마지막 회차.

남은 문제는 세문제

기초트레이닝 문제가 끝나면 어떤 도전과제가 생길지 기대된다

 

1. 그림 확대

10보다 작은 양의 정수 k만큼 2차원배열을 늘리는 문제

  var newPicture: [String] = []
  for i in 0 ..< picture.count {
    for _ in 0 ..< k {
      newPicture.append(picture[i])
    }
  }
  
  for line in 0 ..< newPicture.count {
    let arr = Array(newPicture[line])
    var newLine: [String.Element] = []
    for i in 0 ..< arr.count {
      for _ in 0 ..< k {
        newLine.append(arr[i])
      }
    }
    newPicture[line] = newLine.map({ String($0) }).joined()
  }
  
  return newPicture

세로로 늘리고 가로로늘리고를 크게 두가지로 나눠서 처리했는데 이걸 한번에 묶어서 처리를 하는게 인기가 좋나보다.... 다른답들 다 그렇게 처리가 되어있다

2. 정수를 나선형으로 배치하기

이중배열을 시계방향으로 돌아가면서 방문하는 문제이다.

class Position {
  var x: Int
  var y: Int
  init(x: Int, y: Int) {
    self.x = x
    self.y = y
  }
  init(_ copy: Position) {
    self.x = copy.x
    self.y = copy.y
  }
}
enum Direction {
  case right, left, down, up
}
func solution(_ n:Int) -> [[Int]] {
  var map: [[Int]] = Array(repeating: Array(repeating: 0, count: n), count: n)
  // 캐릭터 현재 x,y 좌표
  let position: Position = .init(x: 0, y: 0)
  var checkNumber: Int = 1
  
  func isMoveRight() -> Bool {
    let startX = position.x
    let startY = position.y
    let nextX = startX + 1
    // 맵 범위밖
    if nextX >= n {
      return false
    }
    // 이미 탐사한 지역
    if map[startY][nextX] > 0 {
      return false
    }
    return true
  }
  
  func isMoveLeft() -> Bool {
    let startX = position.x
    let startY = position.y
    let nextX = startX - 1
    if nextX < 0 {
      return false
    }
    if map[startY][nextX] > 0 {
      return false
    }
    return true
  }
  
  func isMoveDown() -> Bool {
    let startX = position.x
    let startY = position.y
    let nextY = startY + 1
    if nextY >= n {
      return false
    }
    if map[nextY][startX] > 0 {
      return false
    }
    return true
  }
  
  func isMoveUp() -> Bool {
    let startX = position.x
    let startY = position.y
    let nextY = startY - 1
    if nextY < 0 {
      return false
    }
    if map[nextY][startX] > 0 {
      return false
    }
    return true
  }
  
  func checkMap(position: Position) -> Bool {
    if map[position.y][position.x] > 0 {
      print("ERROR!!!!!")
      return false
    }
    map[position.y][position.x] = checkNumber
    checkNumber += 1
    return true
  }
  
  // 시작 위치
  _ = checkMap(position: position)
  
  // 순서 right down left up
  var direction: Direction = .right
  while true {
    
    let beforPosition: Position = .init(position)
    
    // 이동 가능 체크
    if !(isMoveRight() || isMoveLeft() || isMoveUp() || isMoveDown()) {
      break
    }
    
    // 이동
    switch direction {
    case .right:
      if isMoveRight() {
        position.x += 1
      } else {
        direction = .down
      }
    case .down:
      if isMoveDown() {
        position.y += 1
      } else {
        direction = .left
      }
    case .left:
      if isMoveLeft() {
        position.x -= 1
      } else {
        direction = .up
      }
    case .up:
      if isMoveUp() {
        position.y -= 1
      } else {
        direction = .right
      }
    }
    
    if beforPosition.x == position.x && beforPosition.y == position.y {
      continue
    }
    // 맵 수정
    _ = checkMap(position: position)
  }
  
  return map
}

이동하는걸 각 기능으로 나눠서 방향지정까지 해놨는데 

솔직히 너무 과했다 쓸모없다

이렇게 해두면 그야 탐색 순서가 달라져도 써먹을 부분이 많겠지만 너무 오버했다

게임을 구현한다고 생각하니 뭔가 필받아서 머리가 나빠졌나보다 

중간에 이동가능체크 라고 넣어둔부분은 

추가로 루프 탈출지점을 깜빡해서 나중에 넣은 부분이라 저런다만 저런건 미리 계산해두고 사용하는게 좋을듯하다 

아니 아예 인덱스부분만 골라서 숫자 올리는게 더 코드가 짧을지도 모르겠다

 

 

3. 정사각형으로 만들기

이중배열을 n형태의 배열로 만드는 문제, 줄수든 줄내의 요소수든 가장 큰값을 기준으로 0을 채워야한다

  var maxECount: Int = 0
  for e in arr {
    maxECount = max(maxECount, e.count)
  }
  
  let targetCount: Int = max(arr.count, maxECount)
  
  var resultArr: [[Int]] = Array(repeating: Array(repeating: 0, count: targetCount), count: targetCount)
  for line in 0 ..< resultArr.count {
    if line >= arr.count {
      continue
    }
    for i in 0 ..< resultArr[line].count {
      if i >= arr[line].count {
        continue
      }
      resultArr[line][i] = arr[line][i]
    }
  }
  
  return resultArr

이번에도 그냥 문제 내용대로 

가장 큰수를 구하고 해당 큰수대로 정사각형 배열을 만들어줬다

문제는 기존 배열을 늘리는거지만 새롭게 만들어준거에 복사해주는것으로 대체.

 

 

그렇게 기초 트레이닝 문제가 끝!

마지막일자꺼는 왜 5문제가 아닐까....

24일날 시작하여 대부분은 25일까지 완료를 짖고  흥미가 떨어져 도전과제로 풀리는것만 풀다보니

일자로만 11일이 걸렸다.

하루에 day가 두개씩 열리는거보면 사이트에서 노린 기초 트레이닝 일자는 대략 14일 정도 2주가 아니였을까?

"사이트의 노림수대로 일자가 끝난거야 " 라며 자신을 위로하며 기초트레이닝 문제를 마무리한다.

반응형

한 이틀 정도만 꽤나 열심히 진행했고 그후로는 그냥 한두문제씩 풀고있는데 끝이보이는것도 신기하다

1. 전국대회선발고사

모든 사람의 등수와 이 사람들의 참가 가능여부를 담은 배열 두개를 넣어 마지막 계산식을 반환하는문제

  var trueIndex: [Int] = []
  for( i,b) in attendance.enumerated() {
    if !b { continue }
    trueIndex.append(i)
  }
  var playerRanks: [Int] = []
  for i in trueIndex {
    playerRanks.append(rank[i])
  }
  playerRanks.sort()
  let top3PlayerRanks = playerRanks[0..<3]
  var top3PlayerIndex: [Int] = []
  for tRank in top3PlayerRanks {
    let index = rank.firstIndex { rank in
      rank == tRank
    }
    guard index != nil else {
      return -1
    }
    top3PlayerIndex.append(index!)
  }
  guard top3PlayerIndex.count == 3 else { return -2 }
  let a = top3PlayerIndex[0], b = top3PlayerIndex[1], c = top3PlayerIndex[2]
  return 10000 * a + 100 * b + c

문제를 읽으면서 순서대로 코드로 옴겼더니 코드가 엄청 길어졌다.

일단 답은 맞아서 타인의 답을 볼수 있게 되었는데 zip을 이용해 원본인덱스와 등수를 저장한후 정렬,필터할 생각은 못했구나

엄청 짧게 가능한거 보면 대단들하다

 

2. 두 수의 합

대학시절 1학년때 가산기 만들던거를 코드로 만들어봐라 하는 문제이다

  /**
   어떻게 할까.
   그냥 더해버리면 Int최대값을 넘어버린다, uint로 하면되지 않을까 했지만 안되고, longlongint로 하면되지않나? uint64도 넘어버리나보다.
   문자열을 잘라서 일부분씩 더하고 넘어가는 자리만 따로 구해서 더하는 방식으로 가야겠다.
   0. 네자리수의 문자열을 int형으로 변환후 더하고 자리수가 넘어가는 수와 분리해 넘어가는수, 합쳐진 네자리숫자를 반환하는 함수를 만든다.
   1. 문자열의 뒤 네자리씩 자른 배열로만든다
   2. 0에서 만든 함수를 돌려서 계산
   3. 각 자리수 위치에 맞게 정렬해서 반환
   - 한자리로 만들어 테스트해볼까?
   */
//  return "\(UInt64(a)! + UInt64(b)!)"
  // return (오버, 더해진값)
  enum cError: Error {
    case outOverRange
  }
  // 한자리 덧셈
  func add(over: Int, a: Int, b: Int) throws -> (Int, String) {
    if a > 9 || b > 9 || over > 9 {
      throw cError.outOverRange
    }
    
    let total: Int = over + a + b
    if total < 10 {
      return (0, String(total))
    }
    
    let totalArr = Array(String(total))
//    let over: Int = .init(String(totalArr[..<(totalArr.count - 1)]))!
    let befor = String(totalArr[..<(totalArr.count - 1)])
    let over = Int(befor)!
    
    return (over, String(totalArr.last!))
  }
  
  let aArr = Array(a).map({ String($0) }).reversed().map { $0 }
  let bArr = Array(b).map({ String($0) }).reversed().map { $0 }
  let maxIndex = max(aArr.count, bArr.count)
  
  var over: Int = 0
  var result: [String] = []
  for i in 0 ..< maxIndex {
    var aNum: Int = 0, bNum: Int = 0
    if i < aArr.count {
      let str = aArr[i]
      aNum = Int(str)!
    }
    if i < bArr.count {
      let str = bArr[i]
      bNum = Int(str)!
    }
    guard let calc = try? add(over: over, a: aNum, b: bNum) else {
      return "error"
    }
    over = calc.0
    result.append(calc.1)
  }
  if over > 0 {
    result.append("\(over)")
  }
  return result.reversed().joined()

만들다 보니 또 엄청길어졌다..........

또 "나는 4자리씩 끊어서 해야지" 라고 마음먹고 시작했으나 막상 1자리가 성공적으로 되니 귀찮아져서 포기

가산기를 만들다보니 디지털논리회로 강의때가 생각나 또 과거회상에 빠져 현재와의 비교되는 과거에 우울해졌다

그때는 이것저것 해보면서 재밌었는데 안타깝다

 

아무튼 그렇게 day25로 구성된 문제집이  day24,25 밖에 안남아서  내일로 끝이날거다 

처음 시작할때 넉넉잡아 일주일정도면 끝나겠지 했던 기초문제가 2주나 걸려버린거 생각하면 누군가 나에게 프로젝트 일정은 너가 생각한거의 두배를 말하면된다라는게 다시금 떠오른다

 

반응형

비가 계속해서 내려서 눅눅함이 사라지지 않는 밤, 사라지지 않는 빗소리 때문인지 머리가 아파서 잊고있었다

자기전에 생각해내어서 다행.

오늘치는 19,20일치인데 20일은 이미 끝나있고 19일치에 두문제가 남았다

어서 두문제만 끝내고 게임이나 해야지

 

1. 배열 만들기 6

배열 순환해서 조건에 맞는 요소만 뽑아내는 문제

  // i의 초기값을 0으로 설정하고 i가 arr의 길이보다 작으면 다음을 반복
  var i = 0
  var stk: [Int] = []
  
  while i < arr.count {
    if stk.isEmpty {
      stk.append(arr[i])
      i += 1
      continue
    }
    
    if stk.last! == arr[i] {
      _ = stk.popLast()
      i += 1
      continue
    }
    
    stk.append(arr[i])
    i += 1
  }
  
  guard !stk.isEmpty else { return [-1]}
          
  return stk
 

눅눅해서 찝찝하고 머리가 지끈지끈 거려서 아무 생각없이 그냥 문제 그대로를 코드로 바꿨더니 하는일에 비해 좀 길어보인다.

그냥 forEach로 돌리거나 filter로 돌려도 될거같기도하고?

 

2. 무작위로 K개의 수 뽑기

제목은 무작위지만 무작위가 아니라 뭐가 랜덤으로 나왔는지 주어진다. 랜덤도 아니였겠지만...

//  var copyArr: [Int] = Set(arr).map {$0}
  var copyArr: [Int] = []
  for num in arr {
    if copyArr.contains(num) {
      continue
    }
    copyArr.append(num)
    if copyArr.count >= k {
      break
    }
  }
  
  while copyArr.count < k {
    copyArr.append(-1)
  }
  
  return copyArr[..<k].map { $0 }

처음엔 그냥 Set으로 중복제거한후 k개수만큼 보내면 되겠다 싶었는데

이게 처음 주어진 배열에서 순서가 변경되면 아웃처리시켜버린다 "저장된 순서대로 주어질 예정이라고 했을 때," 라는 정의를 또 제대로 안읽고 넘겨버려서 그만 또 틀렸다

레벨0문제인데 뭐이리 자주 틀리는지

아무튼 그렇게 오늘치도 끝!

이제 남은 문제는 6문제!

물론 미리 문제를 풀지 않을거기에 이번주내내 할거같다

반응형

+ Recent posts