Exploring MusicKit and Apple Music API
Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!
WWDC 2021 concluded a few weeks ago, and it was held online again with feature-packed content. With each session on SwiftUI delivering the gist of what the team surprised us with, we love the improvements and rich features announced. This year, Apple focused on supporting deeper adoption of the framework for our apps.
We’ll start from the same project created in Build a Music Chat iOS App Using SwiftUI while working through the new features of SwiftUI Release 3. Then, we’ll look at how the SwiftUI team helped me remove the custom code by integrating them into the framework itself.
Note - You need a paid developer account, and Apple Music installed on your device to follow this article. Also, this article requires Xcode 13.0+ and iOS 15.0+
TonesChat
In Build a Music Chat iOS App Using SwiftUI, we created a music chat app where you can listen to your dearest music while sharing and chatting about it. The sample app used SwiftUI Release 2 extensively, with custom views for things that the framework lacked, for example, search bar.
We’ll start off from the same project, adding new features and improvements introduced this year. Please download the starter project and explore the contents under the Initial folder. It consists of helper views, a few extensions and the model for the API data.
Note - You’ll need to follow the previous article to add developer token for Apple Music and API key for Stream.
Open the initial project in Xcode. First, select the project from the Project Navigator, and bump up the deployment target to iOS 15. This ensures we can build the new exciting stuff on the latest iOS version.
Run the app to ensure that it works. Now, let’s get started with what surprise the framework has for us!
AsyncImage
As the name suggests, AsyncImage is used to download and display images asynchronously. Earlier, we ideally used SDWebImageSwiftUI, but with this version of SwiftUI, we can safely remove the dependency for primary use-cases.
First, delete the package from going to Project Navigator → Swift Packages.
Also, remove import SDWebImageSwiftUI
from MusicCardView
. We’ll get an error regarding WebImage
.
We’ll replace it with AsyncImage
. There are multiple initialisers provided to us, and the simplest one is -
init(url: URL?, scale: CGFloat = 1) where Content == Image
Looking at the syntax -
struct ArtworkImage: View {
var url: URL?
var body: some View {
AsyncImage(url: url)
}
}
We provide the view with a URL, and it automatically fetches and displays the remote image with a default placeholder. To have more control over the phase of the asynchronous image loading operation, we use -
init(url: URL?, scale: CGFloat = 1, transaction: Transaction = Transaction(), content: @escaping (AsyncImagePhase) -> Content)
This helps to add a transaction for passing an animation and defining custom views for loading, success and failure.
For the artwork to be loaded asynchronous in our app, we’ll use a handy wrapper over AsyncImage
-
struct ArtworkImage<Content>: View where Content: View {
private let url: URL?
private var content: (_ image: Image) -> Content
public init(url: URL?, @ViewBuilder content: @escaping (_ image: Image) -> Content) {
self.url = url
self.content = content
}
var body: some View {
if let url = url {
// 1
AsyncImage(url: url, transaction: .init(animation: .spring())) { phase in
switch phase {
// 2
case .empty: progressView()
// 3
case .success(let image): content(image.resizable())
// 4
case .failure(let error as NSError): errorView(with: error)
// 5
@unknown default: unknownView()
}
}
} else {
Text("Wrong URL")
}
}
private func progressView() -> some View {
ProgressView().transition(.opacity.combined(with: .scale))
}
private func errorView(with error: NSError) -> some View {
ZStack {
Color.red.transition(.opacity.combined(with: .scale))
Text(error.localizedDescription).foregroundColor(.white)
}
.transition(.opacity.combined(with: .scale))
}
private func unknownView() -> some View {
Color.gray.transition(.opacity.combined(with: .scale))
}
}
Going through the ArtworkImage
-
- We start with the
AsyncImage
that takes theurl
as the parameter. Andtransaction
for passing the animation while switching between the phases. - In the case of the empty phase, we show a
ProgressView()
. - In the case of a successful image, we pass back the image.
- In case of an error, we show red background with the error description.
- We add an
@unknown default
case for an additional unknown value ofAsyncImagePhase
that maybe added in future versions.
After adding the above code in a new SwiftUI file, head over to MusicCardView
. Remove WebImage(url: url)
and the modifiers associated with it. Replace it with the following -
ArtworkImage(url: url) { image in
image
.scaledToFit()
.transition(.opacity.combined(with: .scale))
}
Note- As of beta 2,
AsyncImage
inList
sometimes cancels image downloads prematurely.
Run the app to see the images load asynchronously, with a funky animation!
Refreshable
On the home screen, we’re displaying the top songs from the US. Maybe, Olivia drops another hit album, and the chart is full of her songs. For this, we need to add functionality to load content on request for updating the data.
SwiftUI 3.0 has added a new feature (missing past years) for supporting pull-to-refresh on iOS and iPadOS. It can be easily used with the refreshable
modifier that configures a refresh action and passes it down through the environment. From the documentation,
Use this action to initiate an update of model data that the modified view displays. Use an
await
expression inside the action. SwiftUI shows a refresh indicator, which stays visible for the duration of the awaited operation.
func refreshable(action: @escaping () async -> Void) -> some View
Go to HomeView
and under onAppear
, add the refreshable modifier -
.refreshable {
viewModel.updateTopSongs()
}
As of Xcode 13.0 beta 1, only List
uses this action. So, open MusicGridView
, and rename it to MusicListView
. Also, replace -
ScrollView(.vertical, showsIndicators: false) {
// LazyVGrid
}
with -
List {
LazyVGrid(columns: items, spacing: 4) {
ForEach(viewModel.songs.shuffled(), id: \.id) { song in
// Content. Shuffling the songs after every refresh.
}
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
Earlier, it wasn’t possible to hide the row separator without some hacks. Now, we’ve a dedicated modifier for this purpose.
SwiftUI also has a new modifier task
that attaches a task to the lifetime of the view. It performs a particular task when the view appears and is cancelled when it disappears.
Replace onAppear
with task
in HomeView
-
.task {
viewModel.updateTopSongs()
}
Now, run the app and refresh to load new top-charting songs!
Material View
Quoting from the What’s new in SwiftUI WWDC 2021,
Materials are used across all of Apple’s platforms and apps to create beautiful visual effects that really emphasize their content, and now you can create them directly in SwiftUI!
Earlier, we used UIVisualEffectView
inside of a UIViewRepresentable
to create a blur effect. Now, we can directly use them inside the background
modifier!
We’ll update the individual card for displaying data into a row, with a blurred background of the artwork for the material effect. Something similar is used in Apple Music as well.
Replace MusicCardView
with the following -
struct MusicCardView: View {
var song: SongData
var body: some View {
HStack {
ArtworkImage(url: url) { image in
image
.scaledToFit()
.transition(.opacity.combined(with: .scale))
}
.cornerRadius(12.0)
.frame(width: 100.0, height: 100.0)
VStack(alignment: .leading, spacing: 4.0) {
Text(name)
.fontWeight(.bold)
.font(.callout)
Text(artistName)
.fontWeight(.light)
.font(.caption)
}
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
.padding(.horizontal)
}
}
private var name: String {
song.attributes.name
}
private var artistName: String {
song.attributes.artistName
}
private var url: URL? {
URL(string: song.attributes.artwork.url, width: 300, height: 300)
}
}
It uses similar code as the previous version, but now we’re enclosing the content inside an HStack
instead of a VStack
and smaller image.
Create a new SwiftUI file with the name MusicRowView.swift. Add the following into it -
struct MusicRowView: View {
var song: SongData
var body: some View {
ZStack {
// 1
ArtworkImage(url: url) { image in
image
.scaledToFill()
.layoutPriority(-1)
.overlay(Color.black.opacity(0.4))
.transition(.opacity.combined(with: .scale))
}
// 2
MusicCardView(song: song)
.background(.thinMaterial)
}
.cornerRadius(12.0)
.padding(4.0)
}
private var url: URL? {
URL(string: song.attributes.artwork.url, width: 300, height: 300)
}
}
Here’s what this view is doing:
- Creates another
ArtworkImage
that fills the whole row. We’re adding an overlay to darken the image. - Adds a background with thin material. This creates a subtle blur effect on the background. These materials automatically blend with the content on top of them.
This generates a beautiful looking blurred background for each row. Try it out!
Search
The current way of working with search is to create a custom view, where we’ve a TextField
, with a cancel button. You can find its implementation in SearchBar.swift.
As the core functionality of TonesChat is to find amazing songs from our friends around the world and search for them, it is critical to have the search feature inbuilt in the framework. This year, a new searchable
modifier makes it effortless to add search in our app.
SwiftUI automatically adds a search field to the appropriate location in our app. You can remove the SearchBar
from SearchView
, and add the searchable
modifier to the NavigationView
. The new implementation looks like this -
struct SearchView: View {
@State private var searchText = ""
@StateObject private var viewModel = SearchSongsViewModel()
var body: some View {
NavigationView {
VStack {
if searchText.isEmpty {
Image("empty_search")
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
} else {
MusicGridView(viewModel: viewModel)
}
}
.navigationTitle("Search")
}
.navigationViewStyle(StackNavigationViewStyle())
// 1
.searchable(text: $searchText)
// 2
.onSubmit(of: .search) {
viewModel.updateSearchSongs(for: searchText)
}
// 3
.onChange(of: searchText) { _ in
if searchText.isEmpty {
viewModel.songs = []
}
}
}
}
Here’s the breakdown:
- We add the
searchable
modifier with the binding ofsearchText
. It automatically adds a search bar in the view at the top. - On submit of a query, we take the text and update the songs list accordingly.
- Whenever the user clicks on the cancel button, we empty the song list to get the default view.
With a few lines of code, we’re able to implement the complete search functionality! That’s a significant improvement!
We can add search suggestions to make it even simpler to search the next song we’re going to binge listen. SwiftUI provides use with another modifier -
func searchable<S>(text: Binding<String>, placement: SearchFieldPlacement = .automatic, suggestions: () -> S) -> some View where S : View
They provide an optional parameter suggestions
where we can list the data. Based on every character that is entered, we hit the hint API of Apple Music to get the optimised result. Clicking on any suggestion searches for that particular hint in the Apple Music catalogue.
Replace the code of SearchView
with the following -
struct SearchView: View {
@StateObject private var viewModel = SearchSongsViewModel()
@State private var searchText = ""
// 1
@State private var selectedHint = ""
var body: some View {
NavigationView {
VStack {
if searchText.isEmpty {
Image("empty_search")
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
} else {
MusicGridView(viewModel: viewModel)
}
}
.navigationTitle("Search")
}
.navigationViewStyle(StackNavigationViewStyle())
.searchable(text: $searchText) {
// 2
ForEach(viewModel.searchHints, id: \.self) { hint in
Button(hint) {
selectedHint = hint
searchText = hint
viewModel.updateSearchSongs(for: hint)
}
}
}
.onSubmit(of: .search) {
viewModel.updateSearchSongs(for: searchText)
}
.onChange(of: searchText) { _ in
updateSearchList()
}
}
// 3
private func updateSearchList() {
if searchText.isEmpty {
viewModel.songs = []
viewModel.searchHints = []
} else {
if selectedHint != searchText {
// 4
viewModel.updateSearchHints(for: searchText)
}
}
}
}
Here’s what’s happening -
- We’ve a
@State
that keeps track of the selected hint. This is to ensure that we don’t find more hints for the selected hint. - We create a list and loop over the search hints. When a user selects a hint, we directly show the search result.
- When the user taps on the cancel or clear text button, we empty the
songs
andsearchHints
array. - For every character typed, we fetch the hints to show.
With this addition of a few lines of code, we’ve a fully functional search! Time to find new music using hints!
Buttons
This release has a lot to do with buttons, especially that now we’ve standard bordered buttons on iOS. It is as simple as adding a buttonStyle
modifier to the element.
The new modifiers are -
buttonStyle
for styling the buttontint
for a specific color appearancecontrolSize
for defining the standard size of a buttoncontrolProminence
for making them stand out
In LoginView
, replace the Login button with the following -
Button(action: login) {
Text("Log in")
.bold()
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(.brand)
.controlSize(.large)
.controlProminence(.increased)
.padding([.horizontal, .bottom])
The button looks more standardised with the new modifiers.
Many More Changes
There’ve been a lot more changes that couldn’t be covered in the post. I recommend you to watch What’s New in SwiftUI.
Custom Swipe Actions
SwiftUI allows us to define custom swipe actions this year, defining it like any other menu. The syntax is similar to a contextMenu
, by defining actions using buttons.
For example, we want to mark favorite few songs that we liked. We can add the following code in MusicGridView
to define a custom action -
MusicRowView(song: song)
.swipeActions(edge: .leading) {
Button {
withAnimation {
// Mark the song favorite
}
} label: {
Label("Favorite", systemImage: "star")
}
.tint(.brand)
}
Here are a few more for your appetite -
- Rendering Markdown -
Text
got more powerful with Markdown formatting directly inline. - New sheet - A new modifier to prevent interactive dismissal of the sheet using
interactiveDismissDisabled(_:)
. Also, there’s a new environment valuedismiss
that you can use to dismiss the screen. - Badge - You can create a badge for displaying in list rows and tab bars. For example, the number of unread messages. Use
badge(_:)
for achieving this functionality. - Accessibility Changes - Many new accessibility changes make it effortless to support accessibility in your SwiftUI app. You can watch SwiftUI Accessibility: Beyond the basics to deliver an exceptional accessibility experience.
Conclusion
With so many changes this year, it’ll be an exciting year ahead to implement them in our app and deliver a smooth experience for our users.
SwiftUI has come a long way from 2019 and iOS 13, but we still have a long way to go. For example, we still require UIKit support for building custom screens and complex apps.
But, SwiftUI is the future, indeed.
I hope you enjoyed this tutorial! Also, if you found this article helpful, let me know!
Exploring MusicKit and Apple Music API
Unlock the full power of MusicKit & Apple Music APIs in your apps with the best guide! Use code musickit-blog for a limited-time 35% discount!