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

 

GitHub - wiwi-git/FirstKeyboard: 키보드 만들어보기...

키보드 만들어보기... Contribute to wiwi-git/FirstKeyboard development by creating an account on GitHub.

github.com

첫 직장을 그만두고 시작해놨던 키보드가 있다.

내 깃허브에서 유일하게 좋아해줬던 사람이 있어 스타를 받았던 프로젝트인데

당시에는 한글 자모 결합하는게 너무 힘들어서 시간을 끌다가 해당 프로젝트 내용 때문인지 extenstion 앱을 만드려는 스타트업에서 제안이와 계약직으로 근무하다가 해당 업체에서 정규직으로 앱 업무를 해줬었었다. 그렇게 내 재취직까지 연결된 프로젝트라 나름 애정이 있는 프로젝트다.

 

fristKeyboard 프로젝트를 만들고 이후에 좀 정리해보겠다고 뒤집어 엎어놔서 솔직히 기억이 하나도 안난다.

우선 기억을 살릴겸 이전에 작성했던 글들을 모아보려고한다.

Swift 커스텀 키보드 진행과 한글 자음/모음 조합( https://wiwi-pe.tistory.com/62)

 

Swift 커스텀 키보드 진행과 한글 자음/모음 조합

요즘 노는 시간 도중도중 공부를 하고있는데 그 공부하고있는것중 ios용 키보드를 하나 만들고있다. 이곳에 쏟아 붇는시간이 얼마되지않아 진도가 많이 느리고 처음에 제대로 공부하고 계획짜

wiwi-pe.tistory.com

 

한글 자모의 유니코드에 대한 글 https://wiwi-pe.tistory.com/92

 

한글 자모의 유니코드에 대한 글

"ᄀ" 과 "ᆨ"은 다른문자입니다. 이전에 키보드를 만들다 말았었다. https://github.com/wiwi-git/FirstKeyboard wiwi-git/FirstKeyboard 키보드 만들어보기... Contribute to wiwi-git/FirstKeyboard development by creating an account o

wiwi-pe.tistory.com

 

 

 

키보드 글을 몇개 안적었었다

큰일났다

프로젝트 개요나 기획쯤은 있을줄 알았는데 ...

최신 키보드 작성글도좀 찾아볼예정이다 

요즘은 죄다 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