스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
What's new in SwiftUI
SwiftUI can help you build better and more powerful apps for iPhone, iPad, Mac, Apple Watch, and Apple TV. Learn more about the latest refinements to SwiftUI, including interface improvements like outlines, grids, and toolbars. Take advantage of SwiftUI's enhanced support across Apple frameworks to enable features like Sign In with Apple. Discover new visual effects, as well as new controls and styles. And find out how the new app and scene APIs enable you to create apps entirely in SwiftUI, as well as custom complications and all new widgets. To get the most out of this session, you should be familiar with SwiftUI. Watch "Introduction to SwiftUI" for a primer.
리소스
관련 비디오
WWDC20
- App essentials in SwiftUI
- Build complications in SwiftUI
- Build SwiftUI apps for tvOS
- Build SwiftUI views for widgets
- Data Essentials in SwiftUI
- Introduction to SwiftUI
- Meet WidgetKit
- Optimize the interface of your Mac Catalyst app
- SF Symbols 2
- SwiftUI로 문서 기반 앱 구축하기
- SwiftUI의 스택, 그리드 및 윤곽선
- UI 타이포그래피의 세부 사항
- What's new in Mac Catalyst
- What's new in Swift
- Widgets Code-along, part 1: The adventure begins
- Widgets Code-along, part 2: Alternate timelines
- Widgets Code-along, part 3: Advancing timelines
-
다운로드
Hello and welcome to WWDC.
My name is Matt Ricketson, and I work on the SwiftUI team. Later on, I'll be joined by my colleague, Taylor. Last year we introduced SwiftUI, a powerful new way to build great user interfaces on all of Apple's platforms. We're incredibly excited to show you what's new in SwiftUI's second major release.
As you'll soon find out, there are a ton of new features this year, much more than we can cover in just one talk. But we'll try to cover as much as we can, and along the way, we'll let you know about other sessions that you can check out to learn more. First up, we'll introduce the new app and widget APIs.
We'll also talk about improvements to displaying lists and collections.
We'll introduce new multi-platform APIs for toolbars and controls...
and also show you new kinds of visual effects for styling your apps.
Finally, we'll discuss new ways for your SwiftUI apps to integrate with the rest of the system. But let's start with apps and widgets. For the first time, you can build an entire app using just SwiftUI instead of embedding your SwiftUI code within a UIKit, AppKit or WatchKit app. Let's take a look. What you see here is a complete SwiftUI app, a simple "Hello, world!" example.
That's right. This is a 100% functioning app. You can build and run this code.
In fact, it's so concise that you can fit the entire app into just 140 characters. But don't let this deceive you. SwiftUI packs a ton of intelligent, automatic, but also customizable behavior into a simple and flexible API for declarative apps. Here I've written an app for keeping track of the books I'm currently reading in my book club. At the bottom, I've written a custom view to represent my app's main user interface. And at the top, I'm using that view as the content of my app's main window. The first thing to notice here is how similar these two declarations are. We designed SwiftUI's new app API to follow the same declarative, state-driven patterns you're already used to in your view code.
In both cases, you define a struct conforming to a protocol.
You can declare data dependencies using properties, and that data is used within the body property, which, for both apps and views, defines their declarative user interface content.
However, you may notice one key difference which is the return type of the app's body property. The body of an app returns a scene, a new concept in SwiftUI that represents pieces of an app's user interface that can be independently displayed by the platform. We've prepared a whole talk that goes into more depth on what scenes are and how they relate to apps and views.
For now, I just want to focus on the scene we're using in this app called WindowGroup. That's because WindowGroup is a powerful example of how scenes in SwiftUI can provide intelligent, multi-platform functionality out of the box.
In our iOS app, WindowGroup is creating and managing a single full-screen window for our application.
But the same code can also run on watchOS, also managing a single full-screen window. Of course, our watchOS app looks different than our iOS app, but the core app structure is the same on both platforms, allowing them to share a single app declaration. In fact, my app will also work on tvOS and on the iPad too. And since iPadOS supports multi-window apps, we get some additional functionality for free...
like being able to create multiple instances of the app that can appear side-by-side.
This also extends to macOS, which also supports multiple windows. I can create new windows using the standard command-N shortcut and gather them up into a single tabbed window.
SwiftUI will even automatically add a new window menu command into my main menu.
And all of this is made possible by this simple app declaration using the new WindowGroup API to define my interface.
SwiftUI supports other types of scenes as well, which can be composed together, like views, to build more complex apps.
Like the new Settings scene available on macOS for adding a preferences window to your Mac app.
The Settings scene will automatically set up the standard preferences command in the app menu and also give the window the correct style treatment.
SwiftUI's scene APIs also support document-based apps like this app that I built for drawing vector shapes.
New this year is the DocumentGroup scene type, which automatically manages opening, editing and saving document-based scenes, supported on iOS, iPadOS and macOS.
On iOS and iPadOS, DocumentGroup will automatically present a document browser if no other main interface is provided.
And on the Mac, DocumentGroup will open a different window for each new document, and also automatically add commands to the main menu for common document actions.
Speaking of menu commands, SwiftUI lets you add additional commands as well using the new commands modifier.
For example, here I've added a custom shape menu for adding new shapes to the canvas. macOS will automatically add custom menus in the correct section of the main menu and will show their keyboard shortcuts which we assigned using the new keyboardShortcut view modifier.
Commands API has a lot more to offer than what we've shown here, such as being able to target commands based on user focus. It's really fun to work with. You can check out our reference documentation to learn more.
There's a lot more to say about apps and scenes, and we've prepared a few other talks to help you dig deeper into these new APIs. "App Essentials in SwiftUI" explains how views, scenes and apps all work together in more depth. And "Document-Based Apps in SwiftUI" dives deep into how to open and manage documents in your app.
To help you build these new apps, we've also updated the "new project" experience in Xcode by adding new multi-platform templates specifically for SwiftUI apps.
These new templates are optimized for multi-platform code, automatically setting up groups for shared code as well as platform-specific components and assets.
Another part of the project experience we're extending is how you configure your app's launch screen.
New this year is the Launch Screen Info.plist key.
This allows you to declare various combinations of standard launch screen components such as default images, background colors and empty top and bottom bars like I've configured here.
You may already be using a storyboard for your launch screen, which still works great, and there's no reason to switch. But for new SwiftUI projects that otherwise don't use storyboards, Launch Screen configurations are a simple alternative. Now let's talk about widgets, an exciting new feature on iOS, iPadOS and macOS.
Widgets are built exclusively with SwiftUI.
You build widgets just like apps and views using a custom struct conforming to the new Widget protocol.
You can make many different types of widgets, like this one that periodically recommends a new album for me to listen to.
Widgets can also be configured with other kinds of data such as Siri intents.
There's a lot to cover when it comes to building widgets, and we have several talks to help you get started. I'd recommend watching "Build SwiftUI Views for Widgets" to learn more.
And finally, you can now use SwiftUI to build custom complications for Apple Watch. You can build a full-color complication like this weekly coffee chart I made and also customize how it looks within a tinted watch face, like this cool blue tint that I like to use.
To learn more, check out "Build Complications in SwiftUI," or if you're new to building complications, I'd recommend starting with "Creating Complications on Apple Watch." Next, let's talk about improvements to displaying lists and collections.
Lists are a vital component of many apps, often representing the primary interface that users interact with.
In this release, lists are gaining some great new features. I'm especially excited about the new support for outlines.
Regular lists enable concise declarations of dynamic, data-driven content.
By providing a children key path to its initializer, a list can now build out recursive outlines of content. By default, this shows up using the expected system-standard styling on macOS... and on iOS and iPadOS.
We hope that easy-to-use outlines can help reduce the need for disruptive push-and-pop navigation patterns within content-focused apps. Along with lists and outlines, it's also common to show collections of content in other kinds of scrollable layouts such as grids.
This year, SwiftUI is adding support for lazy-loading grid layouts which can be composed with scroll views to create smooth-scrolling grids of content.
Grids are powerful layouts that support a variety of different configurations, such as adapting the number of columns to fit the available space like we see here in both landscape and portrait.
Or forcing a fixed number of columns that can each have their own sizing parameters, like this example that sticks with four columns in every orientation. And, of course, SwiftUI also supports horizontally scrolling grids. We're also exposing lazy-loading versions of the existing vertical and horizontal stack layouts, which are great for building custom scrollable layouts like this asymmetric gallery of images. Let's take a closer look.
Here we're using a lazy vertical stack containing all of our gallery content.
We're also using the new view builder support for switch statements, allowing us to easily alternate between different image layouts within the stack such as the single large image shown at the top...
the asymmetric groups of three images...
and the shorter rows of smaller images.
Together, composed with a lazy-loading vertically scrolling stack, they form a seamless gallery. Lists and collections are powerful features of SwiftUI, and we've only scratched the surface of what they're capable of in this talk. To learn more, you should really check out our talk on stacks, grids and outlines. And now to talk about toolbars and controls, I'll hand things over to Taylor. Thank you, Matt. It is so cool to see how easy it is to have our app's model come to life using SwiftUI with things like the new DocumentGroup and new collection views. Now let's jump into the powerful toolbar support in SwiftUI and new ways to customize controls. Toolbars and apps across our platforms have some amazing new updates, from their beautiful new look in macOS Big Sur to the updated iPad system experience to the primary actions in watchOS. And this year, SwiftUI has a new API for constructing all of these using the new toolbar modifier.
Toolbar items consists of the same views you use throughout the rest of SwiftUI, in this case, a button.
They'll be placed in idiomatic locations by default but can be explicitly customized through the use of toolbar items. In this case, the primary action is the default placement on watchOS, but there are other placements as well. For instance, confirmation and cancellation modal actions. These are examples of semantic placements where you're describing to SwiftUI the role that these toolbar items have, and SwiftUI automatically figures out the right spot. Another example is the principal placement to give an item prominence in your app, as you see here on iPad...
and on macOS.
Toolbar items can have positional placements where you want to have that extra level of design control over where your items are placed. Particularly in narrow size classes, it's common to have items in bottom toolbars, and the literal bottomBar placement allows you to explicitly specify that.
You've probably noticed in a few of these examples the use of a new label view in SwiftUI. Let's take a closer look at that. This is a combined representation of a title and an icon that can be used to label UI elements. Here we have a string used as a localization key for its title and the name of a system image, or SF Symbol. And this year, not only are symbols available on macOS, but there are hundreds of new ones available for your apps to use. The "SF Symbols 2.0" talk goes into more detail on all the new enhancements to symbols this year. This construction of Label is actually a convenience for its full form, which is using any view for that title and icon.
And its power comes from the semantics it provides for that title and icon, so they can be treated appropriately based on where they're used. So, returning to our toolbar example, in the context of a toolbar, by default it'll just be the icon visually presented as that button's label and the title used for accessibility purposes.
This behavior extends from toolbars, to context menus, to lists.
Now, this list contains multiple rows of labels. The titles are perfectly aligned, regardless of image size and the power of Labels really shines when using different dynamic type sizes. This is showing the layout for the default large size category and as that changes to extra extra large, both the icon and title update automatically, including nicely reflowing the text and growing the list rows. An even further specialization happens at the larger accessibility sizes.
At those, the labels have updated text wrapping around the icon to maximize the amount of visible text.
Now, with contexts like toolbars having clean, icon-only styles of labeling elements, providing additional help or context for those is more important than ever.
With the new help modifier, you can attach these descriptions of what effect a control will have and that will manifest as Tool Tips on macOS.
What's really cool is that this modifier is available on all platforms as it also provides an accessibility hint to provide an even better voice over experience for your app everywhere. Here we can see a similar experience for an app on the phone for that same toolbar item. Progress button. Record new progress entry. It is so cool how our SwiftUI declarations can naturally improve the experience of our app for everyone.
Now, another new way of bringing more flexibility and power to how people interact with your controls is using the keyboard shortcut modifier. These are most often used for scene commands as it's critical for allowing those commands to be accessible via keyboard shortcut on iPad and on macOS through the main menu, like Matt showed earlier.
However, keyboard shortcuts can also be used for other controls that are shown on screen, such as creating Cancel and default action buttons that have keyboard shortcuts of Escape and Return keys.
From keyboards, to TV remotes, to the watchOS digital crown, focus drives how these indirect inputs are routed in your app. And using the new default focus support, your app can now control where focus starts on screen and how that default might change alongside your app state. The "SwiftUI for tvOS" session goes into more detail on using that new support, as well as other tips for crafting a great tvOS app using SwiftUI.
Last but not least, there are a couple of new controls that you can now use throughout your app. First, there are progress views. These can be used to display determinate and indeterminate progress over time. There are both linear and circular style progress views, the latter enabling everyone's favorite, spinning style, as a display of indeterminate progress.
A similar new control are Gauges. Gauges are used to indicate the level of a value relative to some overall capacity.
Here I have a circular watchOS gauge for tracking the acidity level of my garden's soil.
Gauge has additional optional customizations. Tomatoes are finicky enough to where I'd really like to see the exact pH level at a glance. So I can add a current value label to allow that to be displayed.
Gauge can also have minimum and maximum value labels. In some cases, those might be image icons, but here I'm just gonna display those pH levels as text.
Now, this code snippet also highlights the new multiple trailing closure syntax in Swift. It allowed the expression of our gauge to grow naturally as it gained additional complexity. It is really nice having a new, expressive way of creating toolbars across all of the platforms my apps support, plus these new means of really fine-tuning the behavior of controls both in and out of toolbars.
Next up, let's take a look at new ways of crafting immersive and fun experiences using SwiftUI. macOS Big Sur has a gorgeous revamp to Notification Center and the new Control Center in the menu bar, both built using SwiftUI.
Control Center features these smooth animations in and out of its different modules using a new feature in SwiftUI that you can use in your own apps.
Here I built a little prototype of UI to gather up my favorite albums. It consists of a scrolling grid of albums and a row of the selected ones. Now, on selection, rather than the albums just popping into that row, I'd really like them to fluidly transition from the grid. And using matched geometry effect, it's really easy. I can apply the matched geometry effect modifier to the albums in both the grid and the selected album row using the album's identifier as the identifier to connect the two views, as well as the namespace that those identifiers are relative to. In this case, it's the namespace associated with the containing view.
And that's really all it takes to create this effect. As an album is removed from one section and inserted to the other, SwiftUI will automatically interpolate their frames as a seamless transition.
Another fantastic new tool is ContainerRelativeShape. This is a new shape type that will take on a similar path of the nearest containing shape. We can see the effect here in a widget for our favorite album.
The clip shape on our album artwork automatically took on a concentric corner radius relative to the shape of the widget and so fits perfectly within it.
We can really get a feel for how cool this is by changing that padding, which effectively changes the offset to that outer container shape, and thus the clipping of our view, using ContainerRelativeShape, reacts beautifully...
automatically maintaining that concentricity based on its offset.
There are a few other enhancements to refine the experience of text related elements as well. Custom fonts will automatically scale with dynamic type changes.
Further, now that images can be embedded within text, they'll act as a unified part of that text, including reacting to dynamic type. And for any custom non-text metrics, such as for layout, there's a new scaled metric property wrapper that automatically scales some base value against the current dynamic type size.
All together, these make it so easy to create responsive, custom layouts that react well in these larger accessibility sizes.
There'll be another talk that goes into detail on this, as well as other advanced font and typographic features that can be used to really make your app shine.
Now, these were a few of the new tools for building creative and reactive custom views. But the enhancements to styling your app don't stop there. Even when using system controls, you can customize them to look and feel at home on your app and make your app stand out from the rest by using a custom accent color.
New this year is the ability to customize that accent color on macOS and new support for customizing that accent color directly in the asset catalog in Xcode 12. This lets you easily specify that color for all of the platforms that your app supports.
Now, this is great for applying a broad theme color across your app. But there are also cases where you might wanna specifically customize the tint of a single control.
Now, by default, cyber icons on iPadOS and macOS follow the app accent color. With a new listItemTint modifier you can customize the tint of those icons per item or even for an entire section.
The same affect applies to macOS sidebars where these modifiers also react appropriately for changes to the system accent color.
This same modifier also applies to watchOS where it's used to tint the standard platter background.
And we've brought this tinting support to other controls as well.
Using new style customizations, controls like buttons and toggles can now be explicitly tinted. Here, the switch fill is customized to follow the overall themed accent color rather than the default green. Now, all of these new views and interactions enable even more polished and fun experiences within your app.
And last, but certainly not least, let's look at new ways your app can integrate and take advantage of functionality and services provided by the system.
This year, SwiftUI has a first-class API for opening URLs, available on all platforms.
One form of this is in a new Link view, which takes the URL to open and the label of the link.
It does what you'd expect, creating a visual element with that label and opening that URL with the default web browser.
But in addition, it can also open universal links directly into other apps. In this case, News.
Links also even work within widgets, where they can even link directly back into content within your main app.
Now, in the context of apps, there are cases where URLs need to be programmatically opened. For these advanced cases, there's also an openURL action in the environment, which can be called with the URL to open in an optional completion handler.
Because it's in the context of a specific view, SwiftUI automatically opens that URL relative to its containing window. In an update to iPadOS 13, SwiftUI gained support for enabling your app to both drag to other apps and receive drops from those apps, making your iPad app even more powerful and integrated.
In iOS 14 and macOS Big Sur, this API is built on top of a new framework that enables stronger typed identifiers for the contents being dragged, using the new Uniform Type Identifiers framework.
This has been adopted throughout SwiftUI, allowing you to take advantage of its features throughout your app, from extending it with your app's custom exported or imported types to introspecting a type. For instance, getting its human presentable description or validating its conformance.
The "Document-Based Apps in SwiftUI" talk has more details, such as the differences between imported and exported types. And there's also some great documentation available on Apple's website.
One last example of enabling your app to integrate with other services is the Sign in with Apple button.
This is again a first-class SwiftUI API provided by AuthenticationServices and available on every platform. What's really cool is that simply by importing AuthenticationServices and SwiftUI together, you get these new APIs. There's no new import or framework needed.
And this is just one example of the many Apple frameworks that are now providing SwiftUI views and modifiers. From video players, to maps, to app clip overlays, it's even easier to bring these advanced features into your SwiftUI app.
Many of these are fully multi-platform including natively on watchOS, meaning when you learn how to use these frameworks for one platform, you can apply that anywhere. And that was a quick summary of some of the new ways your apps can integrate and take advantage of the various system features now available in SwiftUI. We've run through a lot of new features and APIs and like Matt mentioned in the beginning, there's just so much more that we didn't have time to talk about. But as one last callout, throughout the talk we came across a few examples where our app's SwiftUI code was made even better from improvements to the language itself. This year's "What's New in Swift" goes into more details on all of the awesome changes in Swift. It has more examples of syntax refinements like builder inference and support for switch and if let inside of builders.
The compiler now has even better diagnostics that helps more quickly pinpoint build errors in your code. And finally, improved performance, such as reductions of code size for your SwiftUI apps and faster code completion.
And these are the types of things that make using SwiftUI that much more enjoyable. We are so excited to share all this with you this year, but lastly, thank you. Thank you for the excitement and passion we've seen from the community. Thank you for the reports on Feedback Assistant, the commentary on social media, the discourse on the forums, the many days of tutorials, and all of the amazing prototypes and explorations people have built. We were just blown away by the excitement we've seen and are really looking forward to what's yet to come. [chimes]
-
-
1:26 - Hello World
@main struct HelloWorld: App { var body: some Scene { WindowGroup { Text("Hello, world!").padding() } } }
-
1:56 - Book Club app
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
4:46 - Settings
@main struct BookClubApp: App { @StateObject private var store = ReadingListStore() @SceneBuilder var body: some Scene { WindowGroup { ReadingListViewer(store: store) } #if os(macOS) Settings { BookClubSettingsView() } #endif } } struct BookClubSettingsView: View { var body: some View { Text("Add your settings UI here.") .padding() } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { NavigationView { List(store.books) { book in Text(book.title) } .navigationTitle("Currently Reading") } } } class ReadingListStore: ObservableObject { init() {} var books = [ Book(title: "Book #1", author: "Author #1"), Book(title: "Book #2", author: "Author #2"), Book(title: "Book #3", author: "Author #3") ] } struct Book: Identifiable { let id = UUID() let title: String let author: String }
-
5:10 - Document groups
import SwiftUI import UniformTypeIdentifiers @main struct ShapeEditApp: App { var body: some Scene { DocumentGroup(newDocument: ShapeDocument()) { file in DocumentView(document: file.$document) } } } struct DocumentView: View { @Binding var document: ShapeDocument var body: some View { Text(document.title) .frame(width: 300, height: 200) } } struct ShapeDocument: Codable { var title: String = "Untitled" } extension UTType { static let shapeEditDocument = UTType(exportedAs: "com.example.ShapeEdit.shapes") } extension ShapeDocument: FileDocument { static var readableContentTypes: [UTType] { [.shapeEditDocument] } init(fileWrapper: FileWrapper, contentType: UTType) throws { let data = fileWrapper.regularFileContents! self = try JSONDecoder().decode(Self.self, from: data) } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { let data = try JSONEncoder().encode(self) fileWrapper = FileWrapper(regularFileWithContents: data) } }
-
5:49 - Custom Commands
import SwiftUI import UniformTypeIdentifiers @main struct ShapeEditApp: App { var body: some Scene { DocumentGroup(newDocument: ShapeDocument()) { file in DocumentView(document: file.$document) } .commands { CommandMenu("Shapes") { Button("Add Shape...", action: addShape) .keyboardShortcut("N") Button("Add Text", action: addText) .keyboardShortcut("T") } } } func addShape() {} func addText() {} } struct DocumentView: View { @Binding var document: ShapeDocument var body: some View { Text(document.title) .frame(width: 300, height: 200) } } struct ShapeDocument: Codable { var title: String = "Untitled" } extension UTType { static let shapeEditDocument = UTType(exportedAs: "com.example.ShapeEdit.shapes") } extension ShapeDocument: FileDocument { static var readableContentTypes: [UTType] { [.shapeEditDocument] } init(fileWrapper: FileWrapper, contentType: UTType) throws { let data = fileWrapper.regularFileContents! self = try JSONDecoder().decode(Self.self, from: data) } func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { let data = try JSONEncoder().encode(self) fileWrapper = FileWrapper(regularFileWithContents: data) } }
-
7:55 - Widgets
import SwiftUI import WidgetKit @main struct RecommendedAlbum: Widget { var body: some WidgetConfiguration { StaticConfiguration( kind: "RecommendedAlbum", provider: Provider(), placeholder: PlaceholderView() ) { entry in AlbumWidgetView(album: entry.album) } .configurationDisplayName("Recommended Album") .description("Your recommendation for the day.") } } struct AlbumWidgetView: View { var album: Album var body: some View { Text(album.title) } } struct PlaceholderView: View { var body: some View { Text("Placeholder View") } } struct Album { var title: String } struct Provider: TimelineProvider { struct Entry: TimelineEntry { var album: Album var date: Date } public func snapshot(with context: Context, completion: @escaping (Entry) -> ()) { let entry = Entry(album: Album(title: "Untitled"), date: Date()) completion(entry) } public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [Entry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = Entry(album: Album(title: "Untitled #\(hourOffset)"), date: entryDate) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } }
-
8:31 - Complications using SwiftUI
struct CoffeeHistoryChart: View { var body: some View { VStack { ComplicationHistoryLabel { Text("Weekly Coffee") .complicationForeground() } HistoryChart() } .complicationChartFont() } } struct ComplicationHistoryLabel: View { ... } struct HistoryChart: View { ... } extension View { func complicationChartFont() -> some View { ... } }
-
9:22 - Outlines
struct OutlineContentView: View { var graphics: [Graphic] var body: some View { List(graphics, children: \.children) { graphic in GraphicRow(graphic) } .listStyle(SidebarListStyle()) } } struct Graphic: Identifiable { var id: String var name: String var icon: Image var children: [Graphic]? } struct GraphicRow: View { var graphic: Graphic init(_ graphic: Graphic) { self.graphic = graphic } var body: some View { Label { Text(graphic.name) } icon: { graphic.icon } } }
-
10:09 - Adaptive grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 176))]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:28 - Fixed-column grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView { LazyVGrid(columns: Array(repeating: GridItem(), count: 4)]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:38 - Horizontal grids
struct ContentView: View { var items: [Item] var body: some View { ScrollView(.horizontal) { LazyHGrid(rows: [GridItem(.adaptive(minimum: 110))]) { ForEach(items) { item in ItemView(item: item) } } .padding() } } } struct Item: Identifiable { var name: String var id = UUID() var icon: Image { Image(systemName: name) } var color: Color { colors[colorIndex % (colors.count - 1)] } private static var nextColorIndex: Int = 0 private var colorIndex: Int init(name: String) { self.name = name colorIndex = Self.nextColorIndex Self.nextColorIndex += 1 } } struct ItemView: View { var item: Item var body: some View { ZStack { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill() .layoutPriority(1) .foregroundColor(item.color) item.icon .resizable() .aspectRatio(contentMode: .fit) .padding(.all, 16) .foregroundColor(.white) } .frame(width: 176, height: 110) } }
-
10:58 - Lazy stacks
struct WildlifeList: View { var rows: [ImageRow] var body: some View { ScrollView { LazyVStack(spacing: 2) { ForEach(rows) { row in switch row.content { case let .singleImage(image): SingleImageLayout(image: image) case let .imageGroup(images): ImageGroupLayout(images: images) case let .imageRow(images): ImageRowLayout(images: images) } } } } } }
-
12:24 - Toolbar modifier
struct ContentView: View { var body: some View { List { Text("Book List") } .toolbar { Button(action: recordProgress) { Label("Record Progress", systemImage: "book.circle") } } } private func recordProgress() {} }
-
12:40 - ToolbarItem
struct ContentView: View { var body: some View { List { Text("Book List") } .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: recordProgress) { Label("Record Progress", systemImage: "book.circle") } } } } private func recordProgress() {} }
-
12:47 - Confirmation and cancellation toolbar placements
struct ContentView: View { var body: some View { Form { Slider(value: .constant(0.39)) } .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Save", action: saveProgress) } ToolbarItem(placement: .cancellationAction) { Button("Cancel", action: dismissSheet) } } } private func saveProgress() {} private func dismissSheet() {} }
-
13:00 - Principal toolbar placement
struct ContentView: View { enum ViewMode { case details case notes } @State private var viewMode: ViewMode = .details var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem(placement: .principal) { Picker("View", selection: $viewMode) { Text("Details").tag(ViewMode.details) Text("Notes").tag(ViewMode.notes) } } } } }
-
13:17 - Bottom bar toolbar placement
struct ContentView: View { var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } } ToolbarItem(placement: .bottomBar) { Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func shareBook() {} }
-
13:38 - Label
Label("Progress", systemImage: "book.circle")
-
14:06 - Label expanded form
Label { Text("Progress") } icon: { Image(systemName: "book.circle") }
-
14:24 - Bottom bar toolbar placement
struct ContentView: View { var body: some View { List { Text("Book Detail") } .toolbar { ToolbarItem { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } } ToolbarItem(placement: .bottomBar) { Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func shareBook() {} }
-
14:36 - Context menu Labels
struct ContentView: View { var body: some View { List { Text("Book List Row") .contextMenu { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } Button(action: addToFavorites) { Label("Add to Favorites", systemImage: "heart") } Button(action: shareBook) { Label("Share", systemImage: "square.and.arrow.up") } } } } private func recordProgress() {} private func addToFavorites() {} private func shareBook() {} }
-
14:39 - List Labels
struct ContentView: View { var body: some View { List { Group { Label("Introducing SwiftUI", systemImage: "hand.wave") Label("SwiftUI Essentials", systemImage: "studentdesk") Label("Data Essentials in SwiftUI", systemImage: "flowchart") Label("App Essentials in SwiftUI", systemImage: "macwindow.on.rectangle") } Group { Label("Build Document-based apps in SwiftUI", systemImage: "doc") Label("Stacks, Grids, and Outlines", systemImage: "list.bullet.rectangle") Label("Building Custom Views in SwiftUI", systemImage: "sparkles") Label("Build SwiftUI Apps for tvOS", systemImage: "tv") Label("Build SwiftUI Views for Widgets", systemImage: "square.grid.2x2.fill") Label("Create Complications for Apple Watch", systemImage: "gauge") Label("SwiftUI on All Devices", systemImage: "laptopcomputer.and.iphone") Label("Integrating SwiftUI", systemImage: "rectangle.connected.to.line.below") } } } }
-
15:28 - Help modifier
struct ContentView: View { var body: some View { Button(action: recordProgress) { Label("Progress", systemImage: "book.circle") } .help("Record new progress entry") } private func recordProgress() {} }
-
16:12 - Keyboard shortcut modifier
@main struct BookClubApp: App { var body: some Scene { WindowGroup { List { Text("Reading List Viewer") } } .commands { Button("Previous Book", action: selectPrevious) .keyboardShortcut("[") Button("Next Book", action: selectNext) .keyboardShortcut("]") } } private func selectPreviousBook() {} private func selectNextBook() {} }
-
16:28 - Cancel and default action keyboard shortcuts
struct ContentView: View { var body: some View { HStack { Button("Cancel", action: dismissSheet) .keyboardShortcut(.cancelAction) Button("Save", action: saveProgress) .keyboardShortcut(.defaultAction) } } private func dismissSheet() {} private func saveProgress() {} }
-
17:08 - ProgressView
struct ContentView: View { var percentComplete: Double var body: some View { ProgressView("Downloading Photo", value: percentComplete) } }
-
17:19 - Circular ProgressView
struct ContentView: View { var percentComplete: Double var body: some View { ProgressView("Downloading Photo", value: percentComplete) .progressViewStyle(CircularProgressViewStyle()) } }
-
17:25 - Activity indicator ProgressView
struct ContentView: View { var body: some View { ProgressView() } }
-
17:32 - Gauge
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } } }
-
17:52 - Gauge with current value label
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } currentValueLabel: { Text("\(acidity, specifier: "%.1f")") } } }
-
18:00 - Gauge with minimum and maximum value labels
struct ContentView: View { var acidity: Double var body: some View { Gauge(value: acidity, in: 3...10) { Label("Soil Acidity", systemImage: "drop.fill") .foregroundColor(.green) } currentValueLabel: { Text("\(acidity, specifier: "%.1f")") } minimumValueLabel: { Text("3") } maximumValueLabel: { Text("10") } } }
-
18:57 - Initial Album Picker
struct ContentView: View { @State private var selectedAlbumIDs: Set<Album.ID> = [] var body: some View { VStack(spacing: 0) { ScrollView { albumGrid.padding(.horizontal) } Divider().zIndex(-1) selectedAlbumRow .frame(height: AlbumCell.albumSize) .padding(.top, 8) } .buttonStyle(PlainButtonStyle()) } private var albumGrid: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: AlbumCell.albumSize))], spacing: 8) { ForEach(unselectedAlbums) { album in Button(action: { select(album) }) { AlbumCell(album) } } } } private var selectedAlbumRow: some View { HStack { ForEach(selectedAlbums) { album in AlbumCell(album) } } } private var unselectedAlbums: [Album] { Album.allAlbums.filter { !selectedAlbumIDs.contains($0.id) } } private var selectedAlbums: [Album] { Album.allAlbums.filter { selectedAlbumIDs.contains($0.id) } } private func select(_ album: Album) { withAnimation(.spring(response: 0.5)) { _ = selectedAlbumIDs.insert(album.id) } } } struct AlbumCell: View { static let albumSize: CGFloat = 100 var album: Album init(_ album: Album) { self.album = album } var body: some View { album.image .frame(width: AlbumCell.albumSize, height: AlbumCell.albumSize) .background(Color.pink) .cornerRadius(6.0) } } struct Album: Identifiable { static let allAlbums: [Album] = [ .init(name: "Sample", image: Image(systemName: "music.note")), .init(name: "Sample 2", image: Image(systemName: "music.note.list")), .init(name: "Sample 3", image: Image(systemName: "music.quarternote.3")), .init(name: "Sample 4", image: Image(systemName: "music.mic")), .init(name: "Sample 5", image: Image(systemName: "music.note.house")), .init(name: "Sample 6", image: Image(systemName: "tv.music.note")) ] var name: String var image: Image var id: String { name } }
-
19:17 - Matched geometry effect Album Picker
struct ContentView: View { @Namespace private var namespace @State private var selectedAlbumIDs: Set<Album.ID> = [] var body: some View { VStack(spacing: 0) { ScrollView { albumGrid.padding(.horizontal) } Divider().zIndex(-1) selectedAlbumRow .frame(height: AlbumCell.albumSize) .padding(.top, 8) } .buttonStyle(PlainButtonStyle()) } private var albumGrid: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: AlbumCell.albumSize))], spacing: 8) { ForEach(unselectedAlbums) { album in Button(action: { select(album) }) { AlbumCell(album) } .matchedGeometryEffect(id: album.id, in: namespace) } } } private var selectedAlbumRow: some View { HStack { ForEach(selectedAlbums) { album in AlbumCell(album) .matchedGeometryEffect(id: album.id, in: namespace) } } } private var unselectedAlbums: [Album] { Album.allAlbums.filter { !selectedAlbumIDs.contains($0.id) } } private var selectedAlbums: [Album] { Album.allAlbums.filter { selectedAlbumIDs.contains($0.id) } } private func select(_ album: Album) { withAnimation(.spring(response: 0.5)) { _ = selectedAlbumIDs.insert(album.id) } } } struct AlbumCell: View { static let albumSize: CGFloat = 100 var album: Album init(_ album: Album) { self.album = album } var body: some View { album.image .frame(width: AlbumCell.albumSize, height: AlbumCell.albumSize) .background(Color.pink) .cornerRadius(6.0) } } struct Album: Identifiable { static let allAlbums: [Album] = [ .init(name: "Sample", image: Image(systemName: "music.note")), .init(name: "Sample 2", image: Image(systemName: "music.note.list")), .init(name: "Sample 3", image: Image(systemName: "music.quarternote.3")), .init(name: "Sample 4", image: Image(systemName: "music.mic")), .init(name: "Sample 5", image: Image(systemName: "music.note.house")), .init(name: "Sample 6", image: Image(systemName: "tv.music.note")) ] var name: String var image: Image var id: String { name } }
-
19:53 - Container Relative Shape
struct AlbumWidgetView: View { var album: Album var body: some View { album.image .clipShape(ContainerRelativeShape()) .padding() } } struct Album { var name: String var artist: String var image: Image }
-
20:34 - Dynamic Type scaling
struct ContentView: View { var album: Album @ScaledMetric private var padding: CGFloat = 10 var body: some View { VStack { Text(album.name) .font(.custom("AvenirNext-Bold", size: 30)) Text("\(Image(systemName: "music.mic")) \(album.artist)") .font(.custom("AvenirNext-Bold", size: 17)) } .padding(padding) .background(RoundedRectangle(cornerRadius: 16, style: .continuous).fill(Color.purple)) } } struct Album { var name: String var artist: String var image: Image }
-
22:08 - Initial Sidebar List
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") Label("Rewards", systemImage: "seal") Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } } .listStyle(SidebarListStyle()) } } }
-
22:17 - List Item Tint in Sidebars
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") .listItemTint(.red) Label("Rewards", systemImage: "seal") .listItemTint(.purple) Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } .listItemTint(.monochrome) } .listStyle(SidebarListStyle()) } } }
-
22:33 - List Item Tint on watchOS
struct ContentView: View { var body: some View { NavigationView { List { Label("Menu", systemImage: "list.bullet") Label("Favorites", systemImage: "heart") .listItemTint(.red) Label("Rewards", systemImage: "seal") .listItemTint(.purple) Section(header: Text("Recipes")) { ForEach(1..<4) { Label("Recipes \($0)", systemImage: "book.closed") } } .listItemTint(.monochrome) } } } }
-
22:46 - SwitchToggleStyle tint
struct ContentView: View { @State var order = Order() var body: some View { Toggle("Send notification when ready", isOn: $order.notifyWhenReady) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } struct Order { var notifyWhenReady = true }
-
23:15 - Link
let appleURL = URL(string: "https://developer.apple.com/tutorials/swiftui/")! let wwdcAnnouncementURL = URL(string: "https://apple.news/AjriX1CWUT-OfjXu_R4QsnA")! struct ContentView: View { var body: some View { Form { Section { Link(destination: apple) { Label("SwiftUI Tutorials", systemImage: "swift") } Link(destination: wwdcAnnouncementURL) { Label("WWDC 2020 Announcement", systemImage: "chevron.left.slash.chevron.right") } } } } }
-
23:56 - OpenURL Environment Action
let customPublisher = NotificationCenter.default.publisher(for: .init("CustomURLRequestNotification")) let apple = URL(string: "https://developer.apple.com/tutorials/swiftui/")! struct ContentView: View { @Environment(\.openURL) private var openURL var body: some View { Text("OpenURL Environment Action") .onReceive(customPublisher) { output in if output.userInfo!["shouldOpenURL"] as! Bool { openURL(apple) } } } }
-
24:44 - Uniform Type Identifiers
import UniformTypeIdentifiers extension UTType { static let myFileFormat = UTType(exportedAs: "com.example.myfileformat") } func introspecContentType(_ fileURL: URL) throws { // Get this file's content type. let resourceValues = try fileURL.resourceValues(forKeys: [.contentTypeKey]) if let type = resourceValues.contentType { // Get the human presentable description of the type. let description = type.localizedDescription if type.conforms(to: .myFileFormat) { // The file is our app’s format. } else if type.conforms(to: .image) { // The file is an image. } } }
-
25:16 - Sign in with Apple Button
import AuthenticationServices import SwiftUI struct ContentView: View { var body: some View { SignInWithAppleButton( .signUp, onRequest: handleRequest, onCompletion: handleCompletion ) .signInWithAppleButtonStyle(.black) } private func handleRequest(request: ASAuthorizationAppleIDRequest) {} private func handleCompletion(result: Result<ASAuthorization, Error>) {} }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.