10

I am making a music app with swift. The app lets users play music through their Apple Music subscription via their Apple Music app. I am able to check whether the user has an Apple Music subscription via:

SKCloudServiceController().requestCapabilities { (capability:SKCloudServiceCapability, err:Error?) in

    guard err == nil else {
        print("error in capability check is \(err!)")
        return
    }

    if capability.contains(SKCloudServiceCapability.musicCatalogPlayback) {
        print("user has Apple Music subscription")
    }

    if capability.contains(SKCloudServiceCapability.musicCatalogSubscriptionEligible) {
        print("user does not have subscription")
    }

}

However: there are scenarios where somebody will, for some reason, have an Apple Music subscription but not have the Apple Music app downloaded on their device. If the user has the subscription but not the device, I want to essentially treat that case as if they do not have a subscription at all, i.e. we cannot play music via Apple Music.

So, I go searching for ways to add a check for if Apple Music is on the user's device. I find this answer: Check whether an app is installed using Swift combined with this resource for finding Apple Music's url scheme and conclude I can check if a user has both an Apple Music subscription and the Apple Music app installed on their device via:

SKCloudServiceController()requestCapabilities { (capability:SKCloudServiceCapability, err:Error?) in

    guard err == nil else {
        print("error in capability check is \(err!)")
        return
    }

    if capability.contains(SKCloudServiceCapability.musicCatalogPlayback) && UIApplication.shared.canOpenURL(URL(string: "music://")!) {
        print("user has Apple Music subscription and has the apple music app installed")
    }

    if capability.contains(SKCloudServiceCapability.musicCatalogSubscriptionEligible) || !UIApplication.shared.canOpenURL(URL(string: "music://")!) {
        print("user does not have subscription or doesn't have apple music installed")
    }

}

The issue is, even after deleting Apple Music from my device, the first case, i.e. the one that prints user has Apple Music subscription and has the apple music app installed is still being called. I believe I have the correct url scheme because when changing "music://" to "musi://", the second case, i.e. the one that prints user does not have subscription or doesn't have apple music installed is being called.

When trying to open URL(string: "music://") with Apple Music deleted via UIApplication.shared.open(URL(string: "music://")!), I am hit with the following alert:

enter image description here

So why is the device saying that I can open URL(string: "music://") even after Apple Music is deleted? Is the URL capable of being opened, but the result is simply the presentation of the above alert? Is this the correct way to confirm that the user has Apple Music installed on their device? Is there even a way to confirm the user has Apple Music installed on their device? If Apple gives users the option to delete the Apple Music app, they should also give developers the ability to check if the app is installed.

David Chopin
  • 2,780
  • 2
  • 19
  • 40
  • I never worked with Apple music myself, but I believe Apple treats this url scheme in a special manner, since it is their own product, thus whenever you hit that url scheme, they better propose to the user to download the app, rather than returning false. Did you try to identify a valid url scheme which would actually open a real album in the Apple Music or play an actual song? Ex.: `URL(string: "music://trackID=3214534")`. Maybe this explicit url scheme would be treated in a usual way, but not trigger an `app restore` alert. – Starsky Nov 11 '19 at 13:24
  • I've tried a number of URLs using valid Apple Music track and artist IDs that don't seem to deep link with Apple music: `URL(string: "music://trackId=1377813289")!`,`URL(string: "music://track=1377813289")`,`URL(string: "music://artist=562555")!`,`URL(string: "music://artistId=562555")!`. The only way I've been able to deep link is via something like `URL(string: "https://music.apple.com/us/artist/562555")!`, but this obviously doesn't help as this is HTTP. – David Chopin Nov 11 '19 at 15:17
  • have you whiteListed "music://" in info.plist with LSApplicationQueriesSchemes?. If not, canOpenUrl is supposed to misBehave. – Karthick Ramesh Nov 11 '19 at 22:40
  • 1
    Unfortunately, whitelisting the scheme in my info.plist doesn't change the behavior at all. My problem is that `UIApplication.shared.canOpenURL(URL(string: "music://")!)` is returning `true` all the time, even if the Apple Music app is deleted. I need it to return `false` when the app is deleted. Whitelisting the url scheme won't fix this issue (I tried it). – David Chopin Nov 12 '19 at 08:04
  • did you find a better solution for that? – Martin Mlostek Dec 08 '19 at 22:40
  • I answered with a work around, but it requires you to make the check in the completion handler for `MPMusicPlayerController.prepareToPlay`, so you actually can't make the check until the user tries to use Apple Music, i.e. play a song. @MartinMlostek – David Chopin Dec 09 '19 at 02:47
  • And does this work for you on iOS 12? On my end the first attempt does not invoke the preparation callback – Martin Mlostek Dec 09 '19 at 08:11
  • `MPMusicPlayerController.prepareToPlay` works in iOS 10.1+ https://developer.apple.com/documentation/mediaplayer/mpmusicplayercontroller/2582424-preparetoplay – David Chopin Dec 09 '19 at 17:22

7 Answers7

5

The best solution I've got, though I expect there is something better out there, is to use MPMusicPlayer.prepareToPlay(completionHandler:) to check if there is an error when trying to play a track:

SKCloudServiceController().requestCapabilities { (capability:SKCloudServiceCapability, err:Error?) in

    guard err == nil else {
        print("error in capability check is \(err!)")
        return
    }

    if capability.contains(SKCloudServiceCapability.musicCatalogPlayback) {
        print("user has Apple Music subscription")
        MPMusicPlayerController.systemMusicPlayer.setQueue(with: ["1108845248"])
        systemMusicPlayer.prepareToPlay { (error) in
            if error != nil && error!.localizedDescription == "The operation couldn’t be completed. (MPCPlayerRequestErrorDomain error 1.)" {
                //It would appear that the user does not have the Apple Music App installed
            }
        }
    }

    if capability.contains(SKCloudServiceCapability.musicCatalogSubscriptionEligible) {
        print("user does not have subscription")
    }

}

I am not sure how this could apply to anybody using Apple Music within their app for anything other than playing tracks, but this seems to definitely work as a check when you are about to play a check. Whenever I am hit with that error, I simply create an alert telling the individual they have an Apple Music subscription but doesn't have the app installed.

Still, it would be great to be able to check without some completion handler as that would allow the boolean check to be integrated into conditional statements (via if capability.contains(SKCloudServiceCapability.musicCatalogPlayback) && hasAppleMusicAppInstalled { //do something }).

David Chopin
  • 2,780
  • 2
  • 19
  • 40
  • Well, this solution works by checking for errors, but it’s a bit hacky and requires executing following code in a completion handler. It’s be better to have a way to simple check, whether or not it’s a part of the MusicKit framework, whether the app itself is installed. – David Chopin Nov 11 '19 at 19:17
  • But yes this answer is a workaround that makes use of errors when preparing to play a track. A developer may need to know if Apple Music is installed without playing a track. – David Chopin Nov 11 '19 at 19:18
  • 1
    is this still the best solution you found? – Martin Mlostek Nov 23 '19 at 13:15
  • Unfortunately, yes – David Chopin Nov 23 '19 at 15:16
1

Luckily Apple provides you a method which returns false if no app installed on the device is registered to handle the URL’s scheme, or if you have not declared the URL’s scheme in your Info.plist file; otherwise, true.

func canOpenURL(_ url: URL) -> Bool

Following i'm posting the url schemes

Open = music://
Open = musics://
Open = audio-player-event://

Add the ones you will further use into your info.plist file.

After this use 'canOpenURL' to check for more information check Apple docs

https://developer.apple.com/documentation/uikit/uiapplication/1622952-canopenurl

Zeeshan Ahmed
  • 834
  • 8
  • 13
1

For the lucky souls that need music app installed to display MPMediaPickerController, the easiest way to check is to see if view was presented. If Music app is missing, it will fail silently.

let mediaPickerController = MPMediaPickerController(mediaTypes: MPMediaType.anyAudio)
mediaPickerController.delegate = self
mediaPickerController.prompt = "prompt"
presenter.present(mediaPickerController, animated: true, completion: nil)

DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { [weak self] () -> () in
    if self?.presenter.presentedViewController == nil {
        self?.callback(.failure(.failedToPresentMusicPickerError))
    }
}
Raimundas Sakalauskas
  • 2,016
  • 21
  • 24
  • Wait, but then this will still pop up something no? – Arnaldo Capo Jul 26 '21 at 10:30
  • Well if there's music app it will pop up MPMediaPickerController. If app is not available the presentedViewController will be nil and you can handle it in the way that is appropriate to your app. In case of the code above it will call callback and pass error code. – Raimundas Sakalauskas Jul 26 '21 at 16:52
  • On some versions of iOS, this leaves your view controller in a limbo where it can't present anything until the impossible-to-present picker has been presented - you'll need to call `dismissViewControllerAnimated` to free it up again. On other versions, the `MPMediaPickerController` is presented (`presentedViewController != nil`), and an error is displayed before it disappears, which will present problems (no pun intended) if you want this to run silently – head in the codes Dec 08 '21 at 05:30
  • Can you be more specific? Which iOS versions did you experience this with? – Raimundas Sakalauskas Dec 09 '21 at 21:27
0

A possible solution is doing the following: Setup a developer token through the Apple Music API (Used so you can query Apple Music REST endpoints). Submit a request to the following StoreKit function (Documentation):

requestUserToken(forDeveloperToken:completionHandler:)

If your developer token is valid and the user token value returned is still nil/null then the device user is not a subscriber of the Apple Music service. An error that is generated with the HTTP status code is 401 (Unauthorized). This still requires you checking an error however does not require to try and play a specific track (Especially for some reason if the content track id your checking against becomes invalid or changed).

For the issue of account signed into the device and has a subscription but not the Music app downloaded: Handle the error upon attempting to play specific content and either provide information to the user or use content that does not require an Apple Music subscription as an alternative when an error occurs.

SierraMike
  • 1,539
  • 1
  • 11
  • 18
  • 2
    Yeah, so I already know how to check if a user has a **subscription**. My answer provided above addresses the actual error handling. The solution I am looking for is **strictly** whether you can tell if the user has the Apple Music app installed to the device **without** having to attempt to play a track. – David Chopin Nov 13 '19 at 20:25
0

Yes, We can check most of the Applications by following these Steps:

  1. Use the Deep URL or URL Scheme for the particular application you want to open, add that into info.plist
  2. Use the same URL and Call this method
    func canOpenURL(_ url: URL) -> Bool
    let url = URL(string: "music://")

    UIApplication.shared.open(url!) { (result) in
       if result {
          // The URL was delivered successfully!
       }
    }
0

You can use this property to detect if Apple Music is installed.

import MediaPlayer
var isAppleMusicInstalled: Bool {
    get async {
        await withCheckedContinuation { continuation in
            MPMusicPlayerController.systemMusicPlayer.setQueue(with: ["1108845248"])
            self.systemMusicPlayer.prepareToPlay { (error) in
                if error != nil && error!.localizedDescription.contains("error 6") {
                    print("Apple Music App not installed")
                    continuation.resume(returning: false)
                } else {
                    continuation.resume(returning: true)
                }
            }
        }
    }
}

Note that it triggers a permission dialog and that you need to add the NSAppleMusicUsageDescription key in your Info.plist with a value like "to check if Apple Music is installed".

Foti Dim
  • 1,303
  • 13
  • 19
-1

Simply do: UIApplication.shared.canOpenUrl(URL(string: “music://”)!)

donkey
  • 1,343
  • 13
  • 32