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는 그냥 몇번 훑어보고 말아서 이참에 공부해야지

반응형

리멤버에 뮤직카우라는 회사에서 네이티브 엔지니어를 구한다는 공고문이 올라왔다

회사 위치나 규모나 하는일이나 맘에 들어서 지원할까! 라고 공고문을 읽는데 요새 네이티브를 안잡은지 오래되고 스위프트5부터 추가된 rx개념과 swiftui의 경험이 없는거나 다름없어서 자격요건이 미묘해서 지원은 아직 안누름

해서 이왕 네이티브 엔지니어로 재취업하고싶으면 해당 자격요건들을 공부하는게 좋을듯하여 해당 내용으로 글을 써보자

 

 

https://career.musicow.com/o/129878

[자격요건]

  • 컴퓨터공학 전공 혹은 이에 상응하는 실무 경험이 있으신 분
  • 3년 이상 또는 이에 준하는 iOS Native 개발 경험이 있으신 분
  • Swift 언어 및 iOS Framework 에 대한 이해가 있으신 분
  • Rest API 의 이해 및 사용 경험이 있으신 분
  • iOS 개발 트렌드에 관심이 많으신 분
  • 문제를 정의하고 적극적으로 해결하고자 하는 의지가 있으신 분

 

[우대사항]

  • SwiftUI 개발 경험이 있으신 분
  • Code based UIKit 개발이 가능한 분
  • Network 와 Concurrency 에 대한 이해가 깊으신 분
  • App Architecture (MVVM, TCA 등) 적용 경험이 있으신 분
  • Reactive Programming(Combine) 에 대한 이해가 있으신 분
  • Unit Test 및 UI Test 작성 및 CI / CD 경험이 있으신 분
  • 음악을 좋아하시고 음악 시장과 문화에 관심이 있으신 분
반응형

'글 쓸 애들' 카테고리의 다른 글

기초 자료구조에 대한 내용  (0) 2023.01.19
Array 조작  (0) 2022.05.18
URL.isDirectory  (0) 2022.05.16
애플 공홈 튜톨  (0) 2022.05.13
네비게이션의 스와이프 백제스쳐가 동작하지 않는다.  (0) 2022.05.04

0. 이글을 쓰기시작한 이유.

예~전에 한참 스위프트 공부에 재미들여서 했을적에 서버드리븐 유아이라는 개념을 듣고 언젠간 공부해봐야지 하고 메모해놨던 일이 있다.

지금이 24년이고, 내가 앱에 입문했을때가 18년이니까 대충 20년쯤이지 아니였을까하는데 뭐 하나 정리를 제대로 안해두는 내 성격상에 여태 까맣게 잊고있다가 최근에 flutter를 공부하다 다시 생각이 났다.

개념자체는 엄청 신기하거나 그러진 않고 앱의 화면에 대한 정보를 서버에서 저장해놓고 앱은 해당 정보를 받아와서 그려는식의 웹뷰를 네이티브 형식으로 구현한 느낌이다.

아 물론 이 설명은 내가 슬쩍 검색해보고 나오는 게시글들의 감상이다

이런식이면 이게 웹앱인지 네이티브앱인지 아리송해지긴하는데 솔직히 요새 서버에서 데이터 받아와서 테이블이나 리스트에 뿌려주는거 보면 어처피 이게 그거 아닌가 싶어서 오히려 왜 방법이 스타트업 사이에서 굴러지고 있는 사람들이 주로 쓰고있지않은건가 의문이든다

요즘은 애플의 앱심사가 정말 빨라졌다지만 그래도 aws에서 파이프라인만들어놔서 빌드하면 자동으로 게시가 되는거 웹페이지에 비교하기에는 너무 천지차이이고 이제막 시작하는 앱이면 이곳저곳 버그가 산재해있거나 바뀌는 아이템으로 앱업데이트를 자주해야하는 상황에선 정말 답답하게된다

이런 상황으로 이전에는 앱시작시 팝업 정보가 있는지 서버에 저장해놨다가 팝업정보가 있으면 앱시작시 띄여주고 해당 팝업 선택시 웹페이지로 연결해주는 방식으로 했었는데 (내 이전직장들이 죄다 망해버려서 오랫동안 쓰이진 않았다) 이런것보단 차라리 server driven ui가 맞지않을까 싶다

UI는 네이티브로 고정해두고 내부 아이템들의 서버변화를 줘서하는거의 크게 다르지 않아서 굳이 기존앱의 변경하는 메리트는 없어보인다

그런데도 이 서버드리븐방식의 글을쓰게된이유는...

블로그에 글을 너무 안올리고있고 거의 반년째 일이 없어 날백수생활을 하고있기때문이다.

뭐라도 해야지....

그렇게 난 flutter로 server driven ui의 구현에 대한 테스트앱을 만들어보기로 한다.

 

 

1. 아이디어 정리

원래는 하루동안 잠깐만 하려고 했던게 조금 쓰고 이제서야 다시 잡았다.

현 시점이 0번항을 적고 나서 시일이 좀 지난 시점이라 당시 어떤 생각을 했는지 좀 잊어버렸다

살짝 짜둔 코드를 보며 다시 아이디어를 정리하고 가야겠다.

내 앱은 우선

전체 구성은 변경하지 않도록 했다 이것도 잘 생각해보면 앱 구성자체를 코드형태로 서버에 저장했다가

스플래시화면에서 받아서 렌더링하는 방법으로 해도 될듯하긴한데 이렇게 하면 차라리 웹앱으로 하는게 맞지않나 싶어서 이것은 포기했다

그렇다면 어느정도 틀만 만들어두고 해당 틀내용만 받아오는식으로 할 생각인데

 

대강 각 탭 페이지는 라우트로 만들어두고 

각 페이지는 카드로 구성되어 있고 각 카드는 몇 종류로 나눠있으며 해당 카드를 미리 만들어두어( 스위프트 uikit의 테이블셀 처럼 ) 

페이지 첫 로드시 각 카드의 순서, 종류, 종류에 따른 내부 컨텐츠 값을 받아와서 화면에 그려주는 방식으로 하려고 한다

 

생각이 깊어질수록 플러터 기본 제공 위젯을 이용하면되는게 아닌가 싶어지기도하는데 이래서 따로 안만들어두고 차라리 웹앱으로 가던가 하는건가....

특정 디자인이 이미 구현되어 있고 꽤나 다른 앱과 다른 유니크한 디자인이라면 일종의 UIkit을 만들어서 제공하는게 있을지도 모르겠다 

대기업 앱엔 있을지도?

 

뭐 아무튼 이번 프로젝트는 방법이 구현이 되는가가 중요하기에 범위자체는 크게 하지 않을거다

많은 앱에서 사용하는 배너용 카드, 단순 글 알림카드?, 사진들어간 좀 큰카드 이렇게 세가지 정도만 화면에서 보여지고

이걸 보여주기위한 넓~직한 카드 읽기용 글카드(사진 포함) -> 단순 글카드와 사진들어간 큰카드 공용으로 쓰고

보통 구현한다는 crud에서 r만 해서 간단히 보여주기 자세히보여주기 만하자

그렇지만 너무 내용이 없어보이면 추가로 이것저것 추가해야겠다

 

그래서 생각나는데로 적은 글들을 정리하자면

1. 메인 페이지 겉부분은 코드로 작성

2. 페이지에 들어가는 내용은 '카드'단위로 나누어서 각 카드별 디자인코드 작성

3. 페이지 로드시 페이지에 들어갈 카드내용을 불러와서 랜더링

4. 카드 내용은 종류, 본문 + 연결될 링크(앱 페이지~ 또는 웹링크~)

 

간단하게 할거라 디자인도 기본으로 제공해주는 위젯을 살짝 래핑하는 식으로 할거다.

일단 다음에 해야지....

 

// ---------

25.01.16일 재시작

프로젝트 시작만 해두고 냅둔애들이 너무 쌓이기시작해서 일단 빠르게 끝낼수 있는 이것먼저 끝내기로함.

위 구성은 잠깐 한두시간한다고 끝낼수 있을거같지않아서 내용을 줄이려한다.

페이지는 2개, 들어가는 스플래시 페이지와, 메인페이지

스플래시는 원래 전체 앱구성을 받고 각 페이지를 웹페이지마냥 코드 읽어서 그려줄려고했는데

스플래시는 하는일 없이 그냥 넘기기로 함

카드구성도 좀 다양하게 만들고 나중에도 써먹을수 있게 템플릿형태로 만들려고 한거같은데 수정

그냥 적당히 4가지 형태로 하고 코드 구현도 chat gpt 무료버전을 이용해 채우도록 하려함

지금이 새벽 3시반 정도되는데 아침되기전에 마무리하고 포스트 업로드하는거로 목표

 

2. 구현

일단 아이디어 답게

페이지가 열리면 api로 화면에 대한 데이터값을 요청 -> 값을 받으면 코드를 그림 

이러는 형식으로 했다

Future<List<CardDatum>> fetchPageData() 를 구현하고

빌드에서 해당 함수를 호출 

Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async => false,
      child: Scaffold(
        body: SafeArea(
            child: FutureBuilder<List<CardDatum>>(
          future: fetchPageData(),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              if (snapshot.data!.isEmpty) {
                return const CircularProgressIndicator();
              }
              var cards = buildCards(snapshot.data!);
              return ListView(
                children: [...cards],
              );
            } else if (snapshot.hasError) {
              return Text('${snapshot.error}');
            }
            return const CircularProgressIndicator();
          },
        )),
      ),
    );
  }

값에 대한 오류가 났을때 재로드하는 식으로 해줄수도 있지만 졸리고 빨리 끝내고 싶어서 로딩바가 계속 굴러가게 했다.

buildCards() 함수는

  List<Widget> buildCards(List<CardDatum> cardData) {
    List<Widget> cardList = [];

    // 카드 밑에 높이8의 빈공간 추가
    for (var datum in cardData) {
      CardType? type = datum.cardType();
      if (type == null) {
        ErrorOBj eobj = ErrorOBj(
            type: "build error", message: "카드 데이터의 타입이 올바르지 않다(${datum.type})");
        print(eobj.toString());
        continue;
      }

      Widget cardWidget = BuildCardUtil().build(type, datum.data);
      cardList.add(cardWidget);
      Widget blank = const SizedBox(height: 8);
      cardList.add(blank);
    }

    return cardList;
  }

따로 타입별 위젯 만드는 클래스를 분리하고 오류로그 찍는 정도 + 각 카드별 공백 잡아줬다

 

카드 빌드하는 함수는

class BuildCardUtil {
  Widget build(CardType type, dynamic data) {
    Map<String, dynamic> map = data as Map<String, dynamic>;

    switch (type) {
      case CardType.banner:
        String imageUrl = map["imageUrl"] as String;
        String link = map["link"] as String;
        return ImageBannerCard(imageUrl: imageUrl, link: link);

      case CardType.profile:
        String name = map["name"] as String;
        String bio = map["bio"] as String;
        String imageUrl = map["imageUrl"] as String;
        return ProfileCard(name: name, bio: bio, imageUrl: imageUrl);

      case CardType.squareImage:
        String imageUrl = map["imageUrl"] as String;
        return SquareImageCard(imageUrl: imageUrl);

      case CardType.titleAndSub:
        String title = map["title"] as String;
        String subtitle = map["subtitle"] as String;
        return TitleSubtitleCard(title: title, subtitle: subtitle);

      default:
        return Container();
    }
  }
}

이런식으로 각 카드클래스를 따로 만들어 연결해주는 방법으로 한단계를 분리하여 확장하기 편하게 했으나...

딱히 앞으로 확장할일이 없기에 괜한일을 했나싶다 한두시간내에 끝내려고했는데 이미 6시가 가까워지고있어서 그냥 손가락 가는데로 타이핑했다

 

추가로 각 카드 클래스들은 gpt를 이용해 뽑아서 간단하게 수정만해줌...

무료버전이라 그런가 애가 실수가 많네...

 

api는 어떻게 해줄까 한참을 고민하고 로컬로 서버 만들어서 할까 아니면 파베에서 공짜로 주는 함수기능을 이용할까 

검색하다가 포스트맨을 이용해 mockApi를 만들어 제공해준다길래 이미 깔려있기도해서 이거 이용해봤다

매우 만족!!!

이런 좋은 기능을 이제야 알았다니..

아래 사진처럼 요청주소, 반환값 이런거 저장해두고

 

아래 mock servers에서 생성해주면!!!!

요청 로그까지 찍어준다!

 

그렇게 아래와같은 예제가 완성되었다

 

실 컨텐츠값은 서버에서 제공하고 앱은 카드형식 만드는거만 지정해서 카드추가와 삭제는 서버에서 지정이 가능한

나름 서버드리븐 앱

원래 구상은 좀더 앱같이 만드려고했으나 어쩌다보니 이렇게 간단한 예제가 되어버리며 프로젝트를 마무리한다.

코드는 내 깃허브에 올릴것이다

 

 

+ 추가 깃링크

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

 

GitHub - wiwi-git/server_driven_test: server driven ui 방식의 테스트

server driven ui 방식의 테스트. Contribute to wiwi-git/server_driven_test development by creating an account on GitHub.

github.com

 

반응형

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 걸고 우리 앱이 소리를 독차지 (*라디오, 팟캐스트 등을 생각하면 됨)

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

 

/---

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

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

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

 

반응형

https://devtalk.kakao.com/t/flutter-misconfigured-error-description-invalid-android-key-hash-or-ios-bundle-id-or-web-site-url/129394

 

Flutter 카카오 로그인 misconfigured, error_description: invalid android_key_hash or ios_bundle_id or web_site_url 에러

kakao_flutter_sdk: ^1.4.2 kakao_flutter_sdk_user: ^1.4.1 사용중입니다. 디버그 모드로 카카오 로그인 기능을 구현해보고자 하는데, 계속 misconfigured, error_description: invalid android_key_hash or ios_bundle_id or web_site_url

devtalk.kakao.com

 

flutter로 앱을 배포이후 안드로이드에서 카카오톡 로그인이 안된다는 리뷰들이 달려있었다.

외주로 만든 앱이기에 요청이 있기전에 뭘 하기에 애매한 상황에서 오랜만에 관련 수정 요청이 들어온상황이라 바로 로그를 확인했으나

invalid android_key_hash or ios_bundle_id or web_site_url 라고 뜨며 카카오톡에서 정보를 받아오지 못하고있었다.

ios에서는 정상적으로 동작하는중이라 android는 또 무슨문젠가 싶은데 여차여차 살펴보니 위 링크의 문제로 보인다

아마 앱번들을 만들면서 해시키가 달라졌던모양

소셜로그인쪽은 앱 초창기에 만들어서 중간에 개발자 계정자체가 달라졌던 상황부터 이 문제가 발생했었던거같다

초기에 검사했던거고 내 개인폰은 ios 이기에 눈치채지못했었나보다

그리고  카카오톡에서 시키는대로 (https://developers.kakao.com/docs/latest/ko/android/getting-started#before-you-begin-add-key-hash-using-keytool) 했는데...

 

동작하지않는다

여기서 알려주는 방법과 내가 디버그로 돌리는 키스토어가 다른모양

빌드후에 알 수 있는 방법이 없다 찾다가 맨위에 링크를 찾었다.

 

KakaoSdk.init( ~~~ )
runApp~~

이후

...
print(await KakaoSdk.origin); // 확인

origin에 대한 주석은 아래와같다

// Origin value in KA header.
//
// Bundle id and Android keyhash for iOS and Android platform, respectively.

카카오톡은 안드로이드의 코틀린으로 작성시만 아래와같이 안내하고 flutter에서는 알려주지 않았던 그 기능과 동일해보인다.

var keyHash = Utility.getKeyHash(this)

 

뭐 그렇게 origin정보를 카카오톡 개발자 페이지의 내 애플리케이션 > 앱설정 > 플랫폼 란의 키해시를 추가해주고

잠시 티타임을 가진후 디버그 모드에서 돌아가는걸 확인

혹시 모르니 추가로 구글 콘솔에서의 SHA-1 인증서 지문을 

echo "${PRINTCERT}" | xxd -r -p | openssl base64 

위 처럼base64 처리해서 해당 키해시값도 추가해줬다.

내 테스트용 안드로이드폰은 최신갤럭시가 아니라 걱정이 되긴하는데 이거로 마무리하고 앱 심사를 맡긴후 회사에 처리완료 메시지를 보냈다.

끝!

 

 

반응형

+ Recent posts