Understanding .musicSubscriptionOffer view modifier

Hi,

I am trying to implement the new view modifier to show a subscription view for AppleMusic. In the #wwdc-10294 (Meet MusicKit for Swift) talk, the use case is very clear and it makes sense. We have an album and if the user wants to play it without an active subscription, we show toggle the binding that makes the view appear.

However, using a single boolean removes flexibility so I am trying to figure out the following:

I have two buttons: "Play" and "Add", which will either play the album as in the talk or add it to the user library using the Apple MusicAPI. In both cases if there is no subscription, I'd like to show the offer. This means I will have two different MusicSubscriptionOffer.Options configuration, one where the messageIdentifier is .playMusic and the other one .addMusic.

This also means that I need to use two viewModifiers .musicSubscriptionOffer resulting in two different bindings (otherwise I assume both view controllers will show when the binding changes?).

Worst case is if I show a list of songs and want to have options for .playMusic and .addMusic for each song, then I need a dynamic amount of bindings for the view modifier. Would I need to handle this like list bindings?

Ideally I'd like an API that allows me to pass in an enum and provide options based on the enum cases like other SwiftUI API (like .sheet). This would allow for dynamic options and only requiring one single .musicSubscriptionOffer.

Is there any solid solution for this at the moment?

Any insights would be helpful, thanks!

Accepted Reply

Hello @sharedRoutine,

Thank you very much for your question about the musicSubscriptionOffer() modifier.

You can definitely achieve what you want with a single musicSubscriptionOffer() modifier.

Here's how it might look like:

struct AlbumActionsRow: View {
    
    // MARK: - State
    
    let album: Album
    
    /// The Apple Music subscription of the current user.
    @State private var musicSubscription: MusicSubscription?
    
    /// The state controlling whether a subscription offer is displayed.
    @State private var isShowingSubscriptionOffer = false
    
    /// The options for the Apple Music subscription offer.
    @State private var subscriptionOfferOptions = MusicSubscriptionOffer.Options(
        affiliateToken: "<affiliate_token>", 
        campaignToken: "<campaign_token>"
    )
    
    
    // MARK: - View
    
    var body: some View {
        HStack {
            Button(action: handlePlayButtonSelected) {
                Text("Play")
            }
            
            Button(action: handleAddButtonSelected) {
                Text("Add")
            }
        }
        .task {
            for await subscription in MusicSubscription.subscriptionUpdates {
                musicSubscription = subscription
            }
        }
        .musicSubscriptionOffer(isPresented: $isShowingSubscriptionOffer, options: subscriptionOfferOptions)
    }
    
    
    // MARK: - Button handlers
    
    private func handlePlayButtonSelected() {
        if musicSubscription?.canPlayCatalogContent == true {
            play()
        }
        else if musicSubscription?.canBecomeSubscriber == true {
            subscriptionOfferOptions.messageIdentifier = .playMusic
            subscriptionOfferOptions.itemID = album.id
            print("Presenting music subscription offer with options: \(subscriptionOfferOptions).")
            isShowingSubscriptionOffer = true
        }
    }
    
    private func handleAddButtonSelected() {
        if musicSubscription?.hasCloudLibraryEnabled == true {
            addToLibrary()
        }
        else if musicSubscription?.canPlayCatalogContent == false && musicSubscription?.canBecomeSubscriber == true {
            subscriptionOfferOptions.messageIdentifier = .addMusic
            subscriptionOfferOptions.itemID = nil
            print("Presenting music subscription offer with options: \(subscriptionOfferOptions).")
            isShowingSubscriptionOffer = true
        }
    }
    
    private func play() {
        …
    }
    
    private func addToLibrary() {
        …
    }
    
}

When I run this code and tap the Play and Add buttons, I see logs like these in the console:

Presenting music subscription offer with options: MusicSubscriptionOffer.Options(messageIdentifier: .playMusic, itemID: 617154241, affiliateToken: <affiliate_token>, campaignToken: <campaign_token>).

[...]

Presenting music subscription offer with options: MusicSubscriptionOffer.Options(messageIdentifier: .addMusic, affiliateToken: <affiliate_token>, campaignToken: <campaign_token>).

I hope this helps.

Best regards,

  • Hello,

    Thank you for your answer! I actually considered using a state variable or a published property in an observable object with a single .musicSubscriptionOffer ViewModifier, however I was worried that I am unnecessarily updating the view by updating the subscription offer options before changing the isShowingSubscriptionOffer and updating the view again. I suppose SwiftUI is smart enough to only rebuild the parts that changed, but this is why I was hesitant to using this approach.

    I will definitely implement it this way for now, thanks again.

  • Hi @sharedRoutine, My pleasure! SwiftUI is engineered to apply all state changes together before running the relevant code to update the UI. This should be totally fine. Best regards,

Add a Comment

Replies

Hello @sharedRoutine,

Thank you very much for your question about the musicSubscriptionOffer() modifier.

You can definitely achieve what you want with a single musicSubscriptionOffer() modifier.

Here's how it might look like:

struct AlbumActionsRow: View {
    
    // MARK: - State
    
    let album: Album
    
    /// The Apple Music subscription of the current user.
    @State private var musicSubscription: MusicSubscription?
    
    /// The state controlling whether a subscription offer is displayed.
    @State private var isShowingSubscriptionOffer = false
    
    /// The options for the Apple Music subscription offer.
    @State private var subscriptionOfferOptions = MusicSubscriptionOffer.Options(
        affiliateToken: "<affiliate_token>", 
        campaignToken: "<campaign_token>"
    )
    
    
    // MARK: - View
    
    var body: some View {
        HStack {
            Button(action: handlePlayButtonSelected) {
                Text("Play")
            }
            
            Button(action: handleAddButtonSelected) {
                Text("Add")
            }
        }
        .task {
            for await subscription in MusicSubscription.subscriptionUpdates {
                musicSubscription = subscription
            }
        }
        .musicSubscriptionOffer(isPresented: $isShowingSubscriptionOffer, options: subscriptionOfferOptions)
    }
    
    
    // MARK: - Button handlers
    
    private func handlePlayButtonSelected() {
        if musicSubscription?.canPlayCatalogContent == true {
            play()
        }
        else if musicSubscription?.canBecomeSubscriber == true {
            subscriptionOfferOptions.messageIdentifier = .playMusic
            subscriptionOfferOptions.itemID = album.id
            print("Presenting music subscription offer with options: \(subscriptionOfferOptions).")
            isShowingSubscriptionOffer = true
        }
    }
    
    private func handleAddButtonSelected() {
        if musicSubscription?.hasCloudLibraryEnabled == true {
            addToLibrary()
        }
        else if musicSubscription?.canPlayCatalogContent == false && musicSubscription?.canBecomeSubscriber == true {
            subscriptionOfferOptions.messageIdentifier = .addMusic
            subscriptionOfferOptions.itemID = nil
            print("Presenting music subscription offer with options: \(subscriptionOfferOptions).")
            isShowingSubscriptionOffer = true
        }
    }
    
    private func play() {
        …
    }
    
    private func addToLibrary() {
        …
    }
    
}

When I run this code and tap the Play and Add buttons, I see logs like these in the console:

Presenting music subscription offer with options: MusicSubscriptionOffer.Options(messageIdentifier: .playMusic, itemID: 617154241, affiliateToken: <affiliate_token>, campaignToken: <campaign_token>).

[...]

Presenting music subscription offer with options: MusicSubscriptionOffer.Options(messageIdentifier: .addMusic, affiliateToken: <affiliate_token>, campaignToken: <campaign_token>).

I hope this helps.

Best regards,

  • Hello,

    Thank you for your answer! I actually considered using a state variable or a published property in an observable object with a single .musicSubscriptionOffer ViewModifier, however I was worried that I am unnecessarily updating the view by updating the subscription offer options before changing the isShowingSubscriptionOffer and updating the view again. I suppose SwiftUI is smart enough to only rebuild the parts that changed, but this is why I was hesitant to using this approach.

    I will definitely implement it this way for now, thanks again.

  • Hi @sharedRoutine, My pleasure! SwiftUI is engineered to apply all state changes together before running the relevant code to update the UI. This should be totally fine. Best regards,

Add a Comment