Issue #889
What’s new in SwiftUI
New EnvironmentValues
TextField inside Alert
List uses UICollectionView
See gist https://gist.github.com/onmyway133/fc08111964984ef544a176a6e9806c18
ButtonStyle composition
Section("Hashtags") {
VStack(alignment: .leading) {
HStack {
Toggle("#Swiftastic", isOn: $swiftastic)
Toggle("#WWParty", isOn: $wwdcParty)
}
HStack {
Toggle("#OffTheCharts", isOn: $offTheCharts)
Toggle("#OneMoreThing", isOn: $oneMoreThing)
}
}
.toggleStyle(.button)
.buttonStyle(.bordered)
}
Customize Charts
struct MonthlySalesChart: View {
var body: some View {
Chart(data, id: \.month) {
BarMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Sales", $0.sales)
)
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
if value.as(Date.self)!.isFirstMonthOfQuarter {
AxisGridLine().foregroundStyle(.black)
AxisTick().foregroundStyle(.black)
AxisValueLabel(
format: .dateTime.month(.narrow)
)
} else {
AxisGridLine()
}
}
}
}
}
Interactive Charts
struct InteractiveBrushingChart: View {
@State var range: (Date, Date)? = nil
var body: some View {
Chart {
ForEach(data, id: \.day) {
LineMark(
x: .value("Month", $0.day, unit: .day),
y: .value("Sales", $0.sales)
)
.interpolationMethod(.catmullRom)
.symbol(Circle().strokeBorder(lineWidth: 2))
}
if let (start, end) = range {
RectangleMark(
xStart: .value("Selection Start", start),
xEnd: .value("Selection End", end)
)
.foregroundStyle(.gray.opacity(0.2))
}
}
.chartOverlay { proxy in
GeometryReader { nthGeoItem in
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(DragGesture()
.onChanged { value in
// Find the x-coordinates in the chart’s plot area.
let xStart = value.startLocation.x - nthGeoItem[proxy.plotAreaFrame].origin.x
let xCurrent = value.location.x - nthGeoItem[proxy.plotAreaFrame].origin.x
// Find the date values at the x-coordinates.
if let dateStart: Date = proxy.value(atX: xStart),
let dateCurrent: Date = proxy.value(atX: xCurrent) {
range = (dateStart, dateCurrent)
}
}
.onEnded { _ in range = nil } // Clear the state on gesture end.
)
}
}
}
}
Half sheet
.sheet(isPresented: $presented) {
Text("Budget View")
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
Grouped Form
Form {
Section {
LabeledContent("Location") {
AddressView(location)
}
DatePicker("Date", selection: $date)
TextField("Description", text: $eventDescription, axis: .vertical)
.lineLimit(3, reservesSpace: true)
}
Section("Vibe") {
Picker("Accent color", selection: $accent) {
ForEach(Theme.allCases) { accent in
Text(accent.rawValue.capitalized).tag(accent)
}
}
Picker("Color scheme", selection: $scheme) {
Text("Light").tag(ColorScheme.light)
Text("Dark").tag(ColorScheme.dark)
}
#if os(macOS)
.pickerStyle(.inline)
#endif
Toggle(isOn: $extraGuests) {
Text("Allow extra guests")
Text("The more the merrier!")
}
if extraGuests {
Stepper("Guests limit", value: $spacesCount, format: .number)
}
}
Section("Decorations") {
Section {
List(selection: $selectedDecorations) {
DisclosureGroup {
HStack {
Toggle("Balloons 🎈", isOn: $includeBalloons)
Spacer()
decorationThemes[.balloon].map { $0.swatch }
}
.tag(Decoration.balloon)
HStack {
Toggle("Confetti 🎊", isOn: $includeConfetti)
Spacer()
decorationThemes[.confetti].map { $0.swatch }
}
.tag(Decoration.confetti)
HStack {
Toggle("Inflatables 🪅", isOn: $includeInflatables)
Spacer()
decorationThemes[.inflatables].map { $0.swatch }
}
.tag(Decoration.inflatables)
HStack {
Toggle("Party Horns 🥳", isOn: $includeBlowers)
Spacer()
decorationThemes[.noisemakers].map { $0.swatch }
}
.tag(Decoration.noisemakers)
} label: {
Toggle("All Decorations", isOn: [
$includeBalloons, $includeConfetti,
$includeInflatables, $includeBlowers
])
.tag(Decoration.all)
}
#if os(macOS)
.toggleStyle(.checkbox)
#endif
}
Picker("Decoration theme", selection: themes) {
Text("Blue").tag(Theme.blue)
Text("Black").tag(Theme.black)
Text("Gold").tag(Theme.gold)
Text("White").tag(Theme.white)
}
#if os(macOS)
.pickerStyle(.radioGroup)
#endif
}
}
}
.formStyle(.grouped)
dropDestination
.dropDestination(payloadType: Image.self) { receivedImages, location in
guard let image = receivedImages.first else {
return false
}
viewModel.imageState = .success(image)
return true
}
AnyLayout
struct ContentView: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
let layout = horizontalSizeClass == .regular ? AnyLayout(HStack()) : AnyLayout(VStack())
layout {
Image(systemName: "star")
Image(systemName: "circle")
Image(systemName: "square")
}
.font(.largeTitle)
}
}
LayoutThatFits https://gist.github.com/ryanlintott/d03140dd155d0493a758dcd284e68eaa
ShapeStyle extensions
struct CalendarIcon: View {
var body: some View {
VStack {
Image(systemName: "calendar")
.font(.system(size: 80, weight: .medium))
Text("June 6")
}
.background(in: Circle().inset(by: -20))
.backgroundStyle(
.blue
.gradient
)
.foregroundStyle(.white.shadow(.drop(radius: 1, y: 1.5)))
.padding(20)
}
}
Text and Image transition
struct TextTransitionsView: View {
@State private var expandMessage = true
private let mintWithShadow: AnyShapeStyle = AnyShapeStyle(Color.mint.shadow(.drop(radius: 2)))
private let primaryWithoutShadow: AnyShapeStyle = AnyShapeStyle(Color.primary.shadow(.drop(radius: 0)))
var body: some View {
Text("Happy Birthday SwiftUI!")
.font(expandMessage ? .largeTitle.weight(.heavy) : .body)
.foregroundStyle(expandMessage ? mintWithShadow : primaryWithoutShadow)
.onTapGesture { withAnimation { expandMessage.toggle() }}
.frame(maxWidth: expandMessage ? 160 : 250)
.drawingGroup()
.padding(20)
.background(.pink.opacity(0.3), in: RoundedRectangle(cornerRadius: 6))
}
}
Grid
Grid {
GridRow {
Text("one")
Text("two")
}
GridRow {
Text("three")
.gridCellColumns(2)
.multilineTextAlignment(.leading)
}
}
Search token and search scope
openWindow
struct DetailView: View {
@Environment(\.openWindow) var openWindow
var body: some View {
Text("Detail View")
.toolbar {
Button {
openWindow(id: "budget")
} label: {
Image(systemName: "dollarsign")
}
}
}
}
struct PartyPlanner: App {
var body: some Scene {
WindowGroup("Party Planner") {
PartyPlannerHome()
}
Window("Party Budget", id: "budget") {
Text("Budget View")
}
.keyboardShortcut("0")
.defaultPosition(.topLeading)
.defaultSize(width: 220, height: 250)
}
}
PhotosPicker
import PhotosUI
import CoreTransferable
struct ContentView: View {
@ObservedObject var viewModel: FilterModel = .shared
var body: some View {
NavigationStack {
Gallery()
.navigationTitle("Birthday Filter")
.toolbar {
PhotosPicker(
selection: $viewModel.imageSelection,
matching: .images
) {
Label("Pick a photo", systemImage: "plus.app")
}
Button {
viewModel.applyFilter()
} label: {
Label("Apply Filter", systemImage: "camera.filters")
}
}
}
}
}
Table
Table(attendeeStore.attendees) {
TableColumn("Name") { attendee in
AttendeeRow(attendee)
}
TableColumn("City", value: \.city)
TableColumn("Status") { attendee in
StatusRow(attendee)
}
}
.contextMenu(forSelectionType: Attendee.ID.self) { selection in
if selection.isEmpty {
Button("New Invitation") { addInvitation() }
} else if selection.count == 1 {
Button("Mark as VIP") { markVIPs(selection) }
} else {
Button("Mark as VIPs") { markVIPs(selection) }
}
}
Customizable Toolbar
.toolbar(id: "toolbar") {
ToolbarItem(id: "new", placement: .secondaryAction) {
Button(action: {}) {
Label("New Invitation", systemImage: "envelope")
}
}
}
.toolbarRole(.editor)
ShareLink
ShareLink(
item: item, preview: SharePreview("Birthday Effects"))
)
onTapGesture with location
Circle()
.frame(width: 100, height: 100)
.onTapGesture { location in
print("location \(location)")
}
MenuBarExtra
struct PartyPlanner: App {
var body: some Scene {
Window("Party Budget", id: "budget") {
Text("Budget View")
}
MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
BulletinBoard()
}
.menuBarExtraStyle(.window)
}
}
Variable SFSymbol
Image(systemName: "speaker.wave.2", variableValue: 0.5)
What’s in UIKit
Use SwiftUI inside UICollectionViewCell
cell.contentConfiguration = UIHostingConfiguration {
VStack {
Image(systemName: "wand.and.stars")
.font(.title)
Text("Like magic!")
.font(.title2).bold()
}
.foregroundStyle(Color.purple)
}
New APIs
SMAppService
An object the framework uses to control helper executables that live inside an app’s main bundle.
LARightStore
A container for data protected by a right.
func storeBackendAccessToken(_ token: Data) async throws {
let loginRight = LARight()
_ = try await LARightStore.shared.saveRight(loginRight, identifier: "access-token", secret: token)
}
ScreenCaptureKit
Use the ScreenCaptureKit framework to add support for high-performance screen recording to your Mac app.