Unleash Your Creativity: Building Your Own Music App with SwiftUI for VisionPro — Step by Step Guide!

The Flutter Way
ITNEXT
Published in
6 min readJul 9, 2023

--

Apple has just announce its very first spatial computer, the VisionPro! I’m sure you’ve heard the buzz — it’s been all over the place! But the best part? For us developers, this is a game-changer! It’s like a whole new world of possibilities opening up! With Xcode 15, which includes the new VisionOS SDK, we can start building apps for this incredible VisionPro right away. Let’s begin our new adventure together by building a music app for VisionOS.

Final sneak peek of our Music app 🥰

Let’s get started

The first thing you need to build an app for VisionOS is Xcode 15. As I’m writing this, you can get the beta version of Xcode 15. If you’re reading this from future, you probably already have Xcode 15 or a newer version. If you don’t have it yet, download it and then open the program. Click on create new project. Then, make sure you’re on the visionOS tab. After that, it’s the same steps as making any other project on Xcode.

Good job on building your first app for VisionPro! Now let’s change this sample app into the one we want. Next task is change the existing code in ContentView with this new one below.

import SwiftUI

struct ContentView: View {
var body: some View {
NavigationSplitView {
// TODO: Side Menu
} detail: {
// TODO: Albums View
}
}
}

#Preview {
ContentView()
}

Side Menu

Right now, it just shows an empty screen. Let’s begin by the side menu. First, we need to create the model for our menu.

import Foundation

struct SideMenuItem: Identifiable, Hashable {
var id = UUID()
var name: String
var icon: String
}

let sideMenuItems: [SideMenuItem] = [
SideMenuItem(name: "Recently Added", icon: "clock"),
SideMenuItem(name: "Artists", icon: "music.mic"),
SideMenuItem(name: "Albums", icon: "square.stack"),
SideMenuItem(name: "Songs", icon: "music.note"),
SideMenuItem(name: "Made For You", icon: "person.crop.square"),
]

Here, we made the side menu model and also created a list that holds our menus. Now Let’s create our side menu.

import SwiftUI

struct SideMenuView: View {
@State private var selectedMenu: SideMenuItem? = sideMenuItems.first!
var body: some View {

List(sideMenuItems) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: item.icon)
.foregroundStyle(.primary)
}
}
.navigationDestination(for: SideMenuItem.self) { item in
// TODO: Album View
}
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
VStack (alignment: .leading) {
Text("Library")
.font(.largeTitle)
Text("All Music")
.foregroundStyle(.tertiary)
}
.padding(.all, 8)
}
ToolbarItem {
Button {} label: {
Image(systemName: "ellipsis")
}

}
}
}
}

The last thing we need is to replace the TODO: Side Menu with SideMenuView() and we are done with our side menu.

Side menu preview

Album view

We’ve finished the side menu, and it looks great! Now, we move on to the next big part, the AlbumsView. This is a simple view that has a search field at the top and a list of Albums. If music is playing, a music controller appears at the bottom. Let’s create the AlbumsView

struct AlbumsView: View {
@State private var searchText: String = ""
var body: some View {
ScrollView {

TextField("Search in Albums", text: $searchText)
.textFieldStyle(.roundedBorder)
.padding(.bottom)

// TODO: Ablums Grid
}
.padding(.horizontal, 24)
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
VStack (alignment: .leading) {
Text("Albums")
.font(.largeTitle)
Text("48 songs")
.foregroundStyle(.tertiary)
}
.padding(.all, 8)
}
ToolbarItem {
Button {} label: {
Image(systemName: "line.3.horizontal.decrease")
}

}
}
// TODO: Media controller
}
}
AlbumsView preview

On the AlbumsView, we’ve got the search field and toolbar items. Now we’re ready to add the albums. But before we do that, we need to create the Album model.

import Foundation

struct Album: Identifiable {
var id = UUID()
var image: String
var title: String
var subTitle: String
}

let albums: [Album] = [
Album(image: "https://i.postimg.cc/ZvLtPzmB/Rectangle-4.png", title: "Sounds of Summer", subTitle: "The Beach Boys"),
Album(image: "https://i.postimg.cc/nMKJfBmF/Rectangle-5.png", title: "Overexposed", subTitle: "Maroon 5"),
Album(image: "https://i.postimg.cc/XpQ6pWxt/Rectangle-6.png", title: "Dreamland", subTitle: "Glass Animals"),
Album(image: "https://i.postimg.cc/G4twDf5t/Rectangle-7.png", title: "Modern Love (Chennai)", subTitle: "Yuvan Shankar Raja, Ila.."),
Album(image: "https://i.postimg.cc/9RZjMqNB/Rectangle-3.png", title: "Formula 1 Theme", subTitle: "Brian Tyler"),
Album(image: "https://i.postimg.cc/RNMzSh1c/Rectangle-8.png", title: "Ved", subTitle: "Ritviz"),
]

We did something similar to what we did for the side menu. Create a variable named columns. This is where we’ll describe the layout we want for the grid.

let columns: [GridItem] = [GridItem(.adaptive(minimum: 160, maximum: 200))]

Replace the TODO: Ablums Grid with the below code

LazyVGrid(columns: columns, spacing: 24) {
ForEach(albums) { album in
Button(action: {}) {
VStack(alignment: .leading) {
AsyncImage(url: URL(string: album.image)) { image in
image.resizable()
} placeholder: {
Rectangle().foregroundStyle(.tertiary)
}.aspectRatio(1, contentMode: .fill)
.scaledToFill()
.cornerRadius(10)

Text(album.title)
.lineLimit(1)
Text(album.subTitle)
.foregroundStyle(.tertiary)
.lineLimit(1)
}
.hoverEffect()
}
.buttonStyle(.plain)
}
}

Back to the ContentView and replace TODO: Albums View with AlbumsView()

Preview with albums

Media Controller

Media controller preview

On VisionOS, we can get this to appear floating in the bottom center by using a toolbar. To do this, we need to set the ToolbarItemGroup placement to bottomOrnament. Replace TODO: Media controller with the below code

.toolbar {
ToolbarItemGroup(placement: .bottomOrnament) {
HStack {
Button(action: {}, label: {
Image(systemName: "backward.fill")
})

Button(action: {}, label: {
Image(systemName: "pause.fill")
})

Button(action: {}, label: {
Image(systemName: "forward.fill")
})

// TODO: Playing Song View

Button(action: {}, label: {
Image(systemName: "quote.bubble")
})

Button(action: {}, label: {
Image(systemName: "list.bullet")
})

Button(action: {}, label: {
Image(systemName: "speaker.wave.3.fill")
})
}.padding(.vertical, 8)
}
}

Let’s create the PlayingSongCardView

struct PlayingSongCardView: View {
var body: some View {
HStack {
AsyncImage(url: URL(string: "https://i.postimg.cc/ZvLtPzmB/Rectangle-4.png")) { image in
image.resizable()
} placeholder: {
Rectangle().foregroundStyle(.tertiary)
}.frame(width: 48, height: 49)
.cornerRadius(6)

VStack (alignment: .leading) {
Text("Kokomo")
Text("The Beach Boys")
.font(.caption2)
.foregroundStyle(.tertiary)
}.frame(width: 160, alignment: .leading)

Button(action: {}, label: {
Image(systemName: "ellipsis")
}).buttonBorderShape(.circle)

}
.padding(.all, 8)
.background(.regularMaterial, in: .rect(cornerRadius: 14))
}
}

Now replace the TODO: Playing Song View with PlayingSongCardView()

Preview with media controller

Tab bars

The last think we are gonna add the Tab bars, which is vertical, floating in a position that’s fixed relative to the window’s leading side in VisionOS. In ContentView, wrap the NavigationSplitView with TabView. Then, add the tabItem to the details.

struct ContentView: View {
var body: some View {
TabView {
NavigationSplitView {
SideMenuView()
} detail: {
AlbumsView()
}.tabItem {
Label("Browse", systemImage: "music.note")
}
.tag(0)
Text("Favorite")
.tabItem {
Label("Favorite", systemImage: "heart.fill")
}
.tag(1)
Text("Playlist").tabItem {
Label("Playlist", systemImage: "play.square.stack")
}
.tag(2)
}
}
}

Note: I had a problem with my preview starting to cache when I was using TabView. I found a solution, though! I just wrapped the preview with NavigationStack and it worked fine. 👇

#Preview {
NavigationStack {
ContentView()
}
}
Final preview of our music app

We did it 🥳! Hopefully, you learned something exciting and new.

Here’s the complete source code 🔥.

If I got something wrong? Let me know in the comments. I would love to improve.

Clap 👏 If this article helps you.

--

--

Want to improve your flutter skill? Join our channel, learn how to become an expert flutter developer and land your next dream job!