Code along with us as we use SwiftUI to build a Mac app from start to finish. Discover four principles all great Mac apps have in common, and learn how to apply those principles in practice using SwiftUI. We'll show you how to create a powerful, flexible sidebar experience and transform lists to tables within a detail view, then discuss best best practices for data organization. Next, we'll explore the simple .searchable modifier and find out how to add support for the toolbar and search. And to close out part one, we'll learn how to build a great multiple-window experience and provide menu bar support.
This is the first session in a two-part Code-Along series. To get the most out of this series, we recommend that you have some basic familiarity with SwiftUI. For more background, watch "Introduction to SwiftUI" from WWDC20.
♪ Bass music playing ♪ ♪ Mathieu Tozer: Hi, I'm Mathieu Tozer, and I'm an engineer on the macOS SwiftUI team. SwiftUI is a multiplatform framework at its core, with the goal of making it easy to build great apps everywhere. When approaching any new concept or API, we take a step back and consider what each platform expects from that feature. What's great about SwiftUI is that its APIs and concepts apply to all platforms, and in this talk we're going to show how those are fine-tuned for the Mac. This is a code-along, where you'll be downloading a skeleton project and writing a Mac app with me. Before we start, let's look over some of the key principles that go into great Mac apps that we'll refer back to while working on our app. These are flexible, familiar, expansive, and precise. Mac Apps are flexible, adjusting to how each of us individually use them. This starts with how we physically use our Macs from keyboards, mice, trackpads, switch controls, even iPads -- and then extends to the software itself. I can customize my workflow by adjusting table columns, sidebars, detail panels, display modes, and windows in a way that suits me best, and the interface will adapt. And at the same time, Mac apps are familiar. Using controls and design patterns consistent with the system makes an app immediately approachable, thanks to a common visual language. For example, the File menu is where I always reach to create new things, and the search bar has a consistent look across all my app toolbars. You can make your app more approachable by zoning out areas of a window, keeping navigation and hierarchy in the sidebar, content in the center, and user functions along the top and right-hand side. But that consistency can still leave room for apps to be unique and stand out among others. You can add customization, such as the app accent color. And when you find you need a custom control, it should be designed to fit in with the system controls. Mac apps are expansive. Large, often multiple displays mean more information can be organized on screen without being hidden away in drill-in style navigation stacks. Concretely, expansive means using controls like sidebars with outline views and thumbnail previews, popovers for transient elements, tabs to toggle between panes of controls, and disclosure groups to display content. Finally, Mac apps are precise. They not only have large windows, but their views also have tighter margins and spacing that results in high densities of content and controls, and these controls are designed to be used with a mouse pointer. All that said, increased density doesn't need to result in increased complexity; an app that serves a simple, single purpose can still be a great Mac app. You probably recognize these ideas from your favorite Mac apps. We'll next be applying these ideas in practice. I'm going to be building a Mac app with you. Hit Pause here and download the project, which will contain the starting and end points for this session. I really like to garden. I think it would be great to have a dedicated app to track my gardens over the years. We're going to build this app for the Mac. We'll take advantage of platform features such as flexible windowing and high information density, enabling me to really interact with my app's data in a way that feels great on macOS. On the left, I have a sidebar showing an outline view of all my garden projects. I can select a garden and see all the plant details in a table view, or as a gallery. Let's start implementing this app. Open up the Session1.workspace in the starter project. We have a garden structure that contains an array of plants, and we have some views and helpers to speed us along, but we'll get started in the ContentView. I'm just going to collapse my sidebar and make some room for my Xcode previews. Our Mac app will have a two-column layout. I'm going to embed this text view in a NavigationView. I'm going to remove the padding, and I'll change the text to a Sidebar. The second column will be for our Plant Table. This flattened hierarchy provides a solid foundation for an expansive navigation experience on the Mac's big screens. Let's get going on our sidebar. If I Command-click, I can extract this view into its own subview, and I'll rename it to "Sidebar". I need access to my store.
And I want to show a list of my gardens in the current year.
And we'll show each garden's name in a label with the leaf systemImage. I'd like to be able to see which gardens are current, put the history in its own section, and then control precisely what I see by clicking disclosure triangles. We'll use a DisclosureGroup, which flattens our navigation system out even more while providing a familiar way to manage complexity. I'm going to change the list to a ForEach, and if I Command-click, I can choose to embed it in a list. We've already got the content we iterate over defined, so we can just clear that out. Now, this expression is equivalent to what we had before, but now we can embed the ForEach in a DisclosureGroup. And for the systemImage, well, this one's a bit of a mouthful but here we go. We'll use the chart.bar.doc.horizontal image. OK, great. I have my gardens organized in an outline structure, giving me the flexibility to control what I see. But I'd like the current group to be open by default, and for the expansion state to persist each time I open the app, which makes my app more familiar. To save the expansionState, we'll add a property to the Sidebar. I'll annotate it with the @SceneStorage property wrapper, providing a key of expansionState.
This will tell SwiftUI to offload this property when the app quits and reload it when the window is restored. Now I'll pass a binding along to the DisclosureGroup for the current year...
...and now the current group is expanded. I think my sidebar is a little tight, so I'm just going to set a minimum width.
I'll show the history in a section, and I'll use a GardenHistoryOutline view I made earlier.
I'll pass that expansionState along as well.
I'm going to add a badge to highlight when my poor plants need water. OK, in order to see the details on the right, I'll need to add selection. We'll add a binding to the ID of the selected garden. And then we can provide that selection as a binding to the list. The Sidebar's parent, the ContentView, will hold on to the value, passing down bindings to the sidebar and the table. So I'm going to copy this up here, too, and I'm going to pop it into @SceneStorage so that it will persist between runs.
And I'll pass it along to the Sidebar as well. I think my sidebar is looking good. I can control what I see with disclosure triangles, and if my Mac restarts, things will be restored to just how I left it. We can get started on the GardenDetail view now. It takes a binding to the selection as well. And if I Command-click, I can jump to the definition of the view. This view already contains properties to our store and some others we'll use shortly. It's also set up to show the garden's name and year as the navigationTitle and Subtitle. We'll implement the table in its own variable on the detail view, and we'll use it here from the body. Let's start by simply showing a list of the garden's plants.
We could display other plant properties in a horizontal stack view, but we have a lot of data to show, and a lot of it is textual. Also, we'd like to add the ability to sort the plants alphabetically or in the order we planted. For these reasons, we'll display plants in a table. A table offers a precise way to view, filter, sort, and edit our data in an expansive UI, making good use of screen space. Generally, if you have visual elements to show and don't need complex sorting, use a list. Otherwise, if you need multiple columns, consider a table. I'm going to replace the list with a table and provide some columns to divide each row by.
And easy as that, I have a table with a single column! So let's add some more columns. I want to know how long it will take for each plant to grow, so I'm going to add a column for that. Instead of providing a key path, I'm going to open up the content closure for the TableColumn, and it will be handed a plant. Now, we can provide any view we like here, but the daysToMaturity property is an integer, so we'll provide a text view with a formatted string. I have a lot of plants, and I'm always planting more. Being able to select and sort the rows will help keep our plants organized. So first we can add a binding to the selection. When making the table sortable, we need to make sure we provide a sortOrder binding and a key path to each column. So we'll need to add a key path to the daysToMaturity column. To really flex the power of the table, let's paste in some more columns from the TableColumns file in the Views folder. They'll let us set some key dates and flag a plant as a favorite.
I'm going to switch off previews for now and run the app.
Look at all this plant data! I can select rows, and I can click on column headers to sort them. This is great, but right now I don't have a way to perform many basic actions needed to manage a garden, such as adding a new plant to the table or marking multiple plants as watered. The toolbar is a familiar place for these kinds of actions -- it's a standard location that macOS users can discover ways to use your app. We can add a toolbar here, starting with a button to add a plant.
We'll give it a label with the title, "Add Plant" and the systemImage "plus". I'm going to build and run, and now I can add a plant to my table! Customization over our app's interface can also be surfaced in the toolbar, such as how we'd prefer to view the garden. We can add the DisplayModePicker here. I've got a lot of plants, and while I can order them, it would be great to narrow down the number of rows quickly and precisely. We always have immediate access to a hardware keyboard on the Mac, so I'm going to add the searchable modifier to the Table, passing in a binding to the searchText. The searchText is a property on the detail view that we use to filter the plants array that gets handed to the table. And that's all we need to do to add search filtering! We've got the structure of our window set up, but I'd really like to be able to see my veggie patch and my backyard flower bed at the same time. Well, it turns out that we already have this ability. I can open up a new window from the File menu, and each window has its own selection and sidebar expansion state. Each toolbar, of course, belongs to its own window, so I can add plants to the veggie patch or the flower bed. My plants need watering regularly. I'd like to be able to sort and filter my plants, select some rows, and mark those as watered all at once. We'll add commands to do these actions to the macOS main menu. The menu is a familiar piece of UI that can even be searched, helping people explore your app. Before adding our custom commands, let's begin by adding some commands that the system provides for us. Move over to the GardenApp file. I'm going to add the commands modifier to the WindowGroup, and add the SidebarCommands(). Now the sidebar can be toggled from the View menu or with a keyboard shortcut. Now for our custom commands. Open the PlantCommands file in the Main Menu folder. I want to be able to send actions to the garden in the front-most window, so I'll need a garden variable. We'll use a @FocusedBinding property wrapper, passing in a key path to a custom property that I've defined in an extension on FocusedValues. The plant command menu items will also need to know which plants are selected in the table so we can check them off as watered, so we'll need to pass the selection up, too. Moving onto the body, you'll notice that commands are declared similarly to views, meaning you can make your own and build a custom tree of commands to represent your menus. First up is the add plant action. Now, while we can already add a plant from the toolbar, the main menu should contain all the possible actions your app can perform, whereas the toolbar, normally just some subset as a convenience. Since this is an action to create something, we'll put it in a familiar place. Using a CommandGroup, we'll place it before the newItem in the File menu. For my watering action, I want to put it in a new CommandMenu called "Plants". This will appear next to the View menu in our app.
These views simply contain buttons that mutate the garden, and modifiers to define the button behavior. We have multiple windows, but only ever one menu bar. I don't want to put carrots in my flower bed, so how can the menu know which garden to send the action to? Back in the GardenDetail view, I'll add the focusedSceneValue modifier to the table, passing in my key path and a binding. I'll do the same for the selection.
This tells the system to expose these values for the given key path when the entire scene is in focus. Finally, in the GardenApp file, we need to add our new commands after the SidebarCommands() we added earlier. SwiftUI will then know to add them to the main menu. I can now insert a plant into the garden in the front-most window from the main menu. I can also select plants that need to be marked as watered, and do so from the Plants menu.
Adding menus to all your app's actions increases the flexibility of your app by enabling keyboard shortcuts and offering different ways to get things done in your app. It also aids the discoverability of actions, empowering people to explore and discover your app's features. Speaking of, in part two, you'll be adding more features and polish to your app with Jeff, including an accent color, drag and drop between tables, and how to take and attach a photo of a plant with an iOS device. We've covered a lot today. We've built the interface for a Mac app from the ground up using SwiftUI, and shown how each component contributes to what can make a great Mac app. Thanks for watching! ♪
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.