회사에서 서비스하는 앱이란건 보통 서버에서 데이터를 저장, 관리하기에 앱 단독으로 데이터 저장은 거의없었다.

개인 로그인 기록이나 편의성을 위한점때문에 userdefaults같은걸 써서 저장하거나 따로 파일을 만들어 저장하는게 편하기에 굳이 새로운 라이브러리를 넣어서 무겁게 돌릴필요가 없었다

그래도 방법은 있으니 혼자서 공부삼아 만들어보는 예제에서는 sqlite를 쓰거나 firebase를 로컬로쓰거나(듣기만했지 만들어보지도 찾아보지도 않았다.) 아니면 애플에서 제공하는 프레임워크인 Coredata를쓰거나! 한다.

그렇게 이글은 애플에서 제공하는 데이터 저장용 프레임워크인 coredata를 uikit이아닌 swiftui로 만들어 붙이는걸 공부하는 글이다.

 

 

4월달 내로 만들어서 내이름으로 처음 등록하려던 앱계획이 있었다. 하지만 정신상태가 흔들리는 일들을 계속 맛보고있는 와중이라 여러번 의욕이 꺽이면서 아직도 한참 멀었다 그앱은 자금사정상 서버를 이용한 기능은 빼고 로컬에서만 돌아가도록할건데 여기서 데이터가 영속적으로 유지해야하는 기능이 존재한다. 

데이터가 영속적으로 존재하려면 위에서 쓴 서버를 이용하거나 텍스트파일로 쓰거나, 로컬db용 라이브러리를 쓰거나해야하는데 자금문제로 서버는 무리고 텍스트파일로 쓰기에는 뭔가 공부도 안될거같아 로컬db로 목표, 그리고 이왕이면 안해본걸 잡아보고싶어서 coredata로 하기로 마음먹었다.

기억이 가물가물하지만 보통 로컬디비가 보통 시작이 이랬을거다. 

1. 디비용 클래스 초기화

2. 초기화시 디비 파일이 없다면 생성 및 스키마 뭐 그런 초기값들 설정

3. 디비 파일이 있다면 로드준비들

4. 해당 기능들을 래핑한 컨트롤러의 생성

5. 이제 컨트롤러를 이용해서 필요할때 백그라운드 스레드로 돌리면서 쿼리를 날리며 ui업데이트

이런식이였을거다.

 

프레임워크를 그냥 제공해주기에 별다른설치나 그런 고민없이 간단히 쓸수있을거라 생각하고 자료좀 찾아봤는데 뭔가 말이많다...

처음 아이폰개발 배웠을때도 이랬었나 고민하게되는 뭔가뭔가들이라 쉽게 손이 안가 그냥 튜토리얼을 따라하며 감을 잡아야겠다 싶어 따라했다

그렇게 대충 배운 내용으로는 

아래와같다

1. 프로젝트 내에 엔티티 정의용 모델파일을 생성한다 - xcdatamodeld 라는 확장자를 가진 파일

2. 정의용 모델파일에서 스키마를 설정 - 서버용의 dbms에서 gui로 편하게 만드는 엔티티라는 느낌이였다

3. 컨트롤러 생성, 컨트롤러지만 컨트롤 기능정의가 아니라 단순 모델파일 로드 - 컨테이너생성

4. enviroment modifier로 managedObjectContext 키값에 만들어둔 컨트롤러에서 컨테이너의 뷰 컨텍스트 지정 

ContentView()
	.environment(\.managedObjectContext, presistenceController.contianer.viewContext)

5. 디비 접근이 필요한곳에서 viewContext 받아주기 + 저장된 값 불러오기

@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(entity: Product.entity(), sortDescriptors: [ 값 정렬 ]
private var products: FetchedResults<Product>

뭐 그렇게 읽고

쓸때는 저기 Product라고 만들어준 엔티티를 생성해줘서 위에서 받아온 viewContext를 이용해 save() 메소드를 호출하면된다.

  private func addProduct() {
    withAnimation {
      let product = Product(context: viewContext)
      product.name = name
      product.quantity = quantity
      
      saveContext()
    }
  } // addProduct
  
  private func saveContext() {
    do {
      try viewContext.save()
    } catch {
      let error = error as NSError
      fatalError("An error occurred: \(error)")
    }
  }// saveContext

참고로 여기서 Product 타입은 엔티티 만드는곳에서 자동으로 생성해서 쓰고있다

이걸 따로 수동으로 만들어서 해줄수도 있음.

수동으로하려면 엔티티 생성해주는곳에서 메뉴얼을 선택해줘야한다. 아래 사진의 Codegen부분이다 Manual과 CategoryExtension이라는게 있던데 카테고리 뭐시기는 아직 안해봤다.

 

위에서 데이터 Fetch해줄때 그냥 전부 가져왔는데 조건문을 넣어서 불러오는것도 당연히 있다.

예제에서는 task 수정자를 이용해 하도록 안내해줬다

.task {
        let fetchRequest: NSFetchRequest<Product> = Product.fetchRequest()
        fetchRequest.entity = Product.entity()
        fetchRequest.predicate = NSPredicate(format: "name CONTAINS %@", name)
        matches = try? viewContext.fetch(fetchRequest)
      }

contain연산자를 이용한 name 검색인데 별건 없다

자동으로 만들어준 Product라 그런가 별에 별게 다 미리 지정되어있다 fetchRequest라던가...

사실 이건 내부 들어가보면 별거 없긴한데 자동이라 편하다.

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Product> {
        return NSFetchRequest<Product>(entityName: "Product")
    }

 

그리고 마지막으로 crud에서 update는 안다룰거고... delete는 뭔가 해준 느낌이 안난다.

      offsets.map { products[$0] }.forEach(viewContext.delete)
      saveContext()

값을 list로 뿌려줬으니 offset값을 바로 가져올수있고 받아온 offset값에 대응하는 products값에서 delete메소드를 불러준후 save 메소드를 불러주는거로 끝이난다

 

기존 로컬디비 다루는법에서 다른점은 

컨트롤러에서의 해주는게 사용하려는 클래스? 스트럭트? (swiftui는 죄다 구조체로 선언해서 용어적으로 뭘 불러야할지 모르겠다 )

사용하려는곳에서 프로퍼티래퍼를 달아줘서 기능을 사용한다는 점일까?

 

일단 이론적인 이야기로는

앱 -

영구컨테이너 -

관리 객체 콘텍스트 ~ 관리 객체 ~ 관리 객체 모델 ~ 엔티티 디스크립션 -

영구 저장소 코디네이터

영구 저장소 객체

데이터베이스

이런 레이어들이 쌓여서 서로 부르고 알고 모르니 하는 설명이 뭔가 많다.

예제에서 다루는 부분을 보면 우선 코디네이터 아래단은 전부 프레임워크에 역할을 맡기는거 같고

영구 컨테이너는 컨트롤러에서 생성, 그 생성할 관리 객체단이 xcdatamodeld파일이라는 느낌? 

반응형

https://www.pcmag.com/news/apples-app-store-rules-have-to-change-after-latest-epic-games-ruling

챗 gpt 요약

📌 주요 요약: Apple, 법원 명령 위반으로 제재
법원 판결: 미국 연방 판사 이본 곤잘레스 로저스(Yvonne Gonzalez Rogers)는 Apple이 2021년 Epic Games와의 반독점 소송에서 내려진 명령을 고의로 위반했다고 판결했습니다. ​
CCN.com
+7
AP News
+7
The Verge
+7

명령 내용: 해당 명령은 Apple이 개발자들에게 외부 결제 수단에 대한 정보를 사용자에게 제공할 수 있도록 허용하고, 앱 외부에서 이루어지는 구매에 대해 30%의 수수료를 부과하지 않도록 요구했습니다.​

Apple의 대응: Apple은 2024년부터 일부 앱 외부 구매에 대해 27%의 수수료를 부과하고, 외부 결제 수단에 대한 사용자 안내를 제한하는 정책을 도입했습니다. ​
MarketWatch

법원의 비판: 로저스 판사는 Apple의 이러한 조치를 명령에 대한 "의도적인 위반"으로 간주하고, Apple의 재무 부사장 알렉스 로만(Alex Roman)이 법정에서 위증했다고 지적했습니다. ​
CCN.com
+2
Business Insider
+2
MarketWatch
+2

추가 조치: 법원은 Apple의 위반 행위를 즉각 중단할 것을 명령하고, 형사 모독죄 가능성을 검토하기 위해 사건을 미국 연방 검찰에 회부했습니다. ​

Epic Games의 반응: Epic Games는 이번 판결을 환영하며, 미국 iOS 앱 스토어에 Fortnite를 다시 출시할 계획을 발표했습니다. ​
Vulture

이러한 판결은 Apple의 앱 스토어 정책에 중대한 변화를 요구하며, 개발자들에게 더 많은 자유를 제공할 수 있는 계기가 될 것으로 보입니다. 추가로 궁금하신 점이나 더 자세한 내용이 필요하시면 언제든지 말씀해 주세요.

 

 

---

주요 내용은 에픽의게임이 결제를 애플에서 제공하는 수수료 30%짜리를 이용하지 않아 게임이 내려간건에 대한 소송이였고

에픽이 이겼습니다

이 일로 ios앱에 기존에는 허용하지 않았던 외부결제 - 웹을 통한 결제또는 또다른 결제 방식이 허용된다면 비싸게 제공하던 수수료를 상당히 많이 아낄수 있게됩니다.

이게 미국에 한정한 일인지 모든 지역에 한정된 일인지는 조금더 지켜봐야알것같습니다

만약 모든지역이라면 애플 자사 결제를 좀 더 싸고 간편히 제공하도록 하지 않을까 싶습니다

기존 방식이 정말 이상해보이는점이 많았기에 고생이 많았습니다만 여러 결제 제공사와 경쟁으로 좋아지길 간절히 바라고있습니다

 

반응형

Struct 'ViewBuilder' requires that 'EmptyTableRowContent<V>' conform to 'View'

Struct 'ViewBuilder' requires that 'TableHeaderRowContent<V, Text>' conform to 'View'

Static method 'buildExpression' requires that 'HStack<TupleView<(Text, Spacer, Text)>>' conform to 'TableRowContent'

 

모델을 좀 통폐합 시키다보니 갑자기 뷰쪽에서 이러한 에러들이 떳다

처음엔 단순히 실수로 괄호가 지워졌나 싶었는데 아무리봐도 틀린부분이 없었다

검색해보니 바인딩에러니 어쩌니 하길래 @붙은 부분을 전부 확인해봐도 모르겠다....

 

그렇게 이것저것 검색하고 안되고, 세번째 에러 부분을 검색하다가

스택오버플로우의 한 댓글을 발견

You seems to do some strange additions in your Text : not sure dayOfWeek is a string. To find out where the problem is : comment your HStack in the ForEach and add them one by one to find the falty one. The compiler gives sometimes error on ForEach when something is wrong in the code inside it. 

 CommentedApr 6, 2024 at 9:47

 

Text부분의 이상한 확장이 있는거 같다는글에 관련된 뷰에 있는 텍스트 코드들을 전부 주석처리하니 에러가 사라짐을 발견

내가 모델을 수정하면서 date 변수를 created와 deadLine으로 분리해놓은걸 깜빡했다.

왜 date변수가 없어요! 라는 에러가 안뜨고 이상한 에러가 떳는지 모르겠다.

preview.date 부분을 멀쩡한걸로 변경해주니 다시 돌아간다....

 

이번달안으로 끝내기로 마음먹은 프로젝트가 진도가 안나간다

반응형

기존 색상을 그대로 쓰려고하니 에러발생 

.foregroundColor(UIColor.systemBlue)
// Cannot convert value of type 'UIColor' to expected argument type 'Color?'

그냥 Color() 생성자를 쓰면 바로 바꿔진다

.foregroundColor(Color(UIColor.systemBlue))

도대체 왜 그냥 자동고침으로 변경이 안되는걸까?...

 

 

참고로 저 Color의 생성자는 아래와같이 선언되어있다.


extension Color {

    /// Creates a color from a UIKit color.
    ///
    /// Use this method to create a SwiftUI color from a
    /// <doc://com.apple.documentation/documentation/UIKit/UIColor> instance.
    /// The new color preserves the adaptability of the original.
    /// For example, you can create a rectangle using
    /// <doc://com.apple.documentation/documentation/UIKit/UIColor/3173132-link>
    /// to see how the shade adjusts to match the user's system settings:
    ///
    ///     struct Box: View {
    ///         var body: some View {
    ///             Color(UIColor.link)
    ///                 .frame(width: 200, height: 100)
    ///         }
    ///     }
    ///
    /// The `Box` view defined above automatically changes its
    /// appearance when the user turns on Dark Mode. With the light and dark
    /// appearances placed side by side, you can see the subtle difference
    /// in shades:
    ///
    /// ![A side by side comparison of light and dark appearance screenshots of
    ///   rectangles rendered with the link color. The light variant appears on
    ///   the left, and the dark variant on the right.](Color-init-3)
    ///
    /// > Note: Use this initializer only if you need to convert an existing
    /// <doc://com.apple.documentation/documentation/UIKit/UIColor> to a
    /// SwiftUI color. Otherwise, create a SwiftUI ``Color`` using an
    /// initializer like ``init(_:red:green:blue:opacity:)``, or use a system
    /// color like ``ShapeStyle/blue``.
    ///
    /// - Parameter color: A
    ///   <doc://com.apple.documentation/documentation/UIKit/UIColor> instance
    ///   from which to create a color.
    public init(_ color: UIColor)
}

 

반응형

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를 만들어서 전달해줘도 될듯싶으나 구조를 변경하기 귀찮아서 그렇게 했다

 

반응형

+ Recent posts