contributors |
---|
zntfdr |
New view that automatically downloads and displays images, also has placeholder, images can be customized via modifiers as usual, can have custom behaviour for error handling.
AsyncImage(url: ...) { image in
image
.resizable()
.aspectRation(contentMode: .fill)
}
task(_:)
lets you attach an async
task to the lifetime of your view: it will be triggered when its view appears, and will be cancelled when this view disappears.
Text(displayValue)
.task {
var results = TextProcessResults()
for try await line in textURL.lines() {
results.accumulateResults(line: line)
}
displayValue = results.textSummary()
}
Pull to refresh via refreshable(action:)
concurrency view modifier, this modifier configures a refresh action (RefreshAction) and passes down through the environment.
Use an await
expression inside the action. SwiftUI shows a refresh indicator, which stays visible for the duration of the awaited operation.
List(mailbox.conversations) {
ConversationCell($0)
}
.refreshable {
await mailbox.fetch()
}
New List
and ForEach
initializers allowing us to get a binding per each element:
struct DirectionsList: View {
@Binding var directions: [Direction]
var body: some View {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
} icon: {
DirectionsIcon(direction)
}
}
}
}
This is back-ported all the way to iOS 13.
listRowSeparatorTint(_:edges:)
- custom row separator colorslistSectionSeparatorTint(_:edges:)
- custom section separator colorslistRowSeparator(_:edges:)
- can be used to hide the separators altogether
New swipeActions(edge:allowsFullSwipe:content:)
view modifier to add swipe actions.
Define each action with Button
s, use the tint(_:)
view modifier to customize the background color (or use the button's role).
List(store.messages) { message in
MessageCell(message: message)
.swipeActions(edge: .leading) {
Button { store.toggleUnread(message) } label: {
if message.isUnread {
Label("Read", systemImage: "envelope.open")
} else {
Label("Unread", systemImage: "envelope.badge")
}
}
.tint(.yellow)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
store.delete(message)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.blue)
Button { store.flag(message) } label: {
Label("Flag", systemImage: "flag")
}
.tint(.green)
}
}
}
All styles now come with a new enum-like syntax:
List {
...
}
.listStyle(.grouped)
instead of:
List {
...
}
.listStyle(GroupedListStyle())
New (macOS-only) style, which alternates the colors of the rows:
List {
...
}
.listStyle(bordered(alternatesRowBackgrounds: true))
New Table
view, supports selection, sorting, and more:
struct ContentView: View {
@State private var characters = StoryCharacter.previewData
var body: some View {
Table(characters) {
TableColumn("") { CharacterIcon($0) }
.width(20)
TableColumn("Villain") { Text($0.isVillain ? "Villain" : "Hero") }
.width(40)
TableColumn("Name", value: \.name)
TableColumn("Powers", value: \.powers)
}
}
}
New searchable(_:text:placement:)
view modifiers, it adds a search field where more appropriate based on the context:
NavigationView {
List {
...
}
.searchable(...)
}
onDrag
now comes with apreview
View
parameter, letting us customize what view to show when dragging.- new
importsItemProviders
view modifier makes a view a drop target that accepts item providers - new
exportsItemProviders
view modifier exposes our app data to external system services
-
Two new rendering modes:
- Hierarchical - like monochrome, but automatically adds multiple levels of opacity to really emphasize the key elements of the symbol
- Palette - gives more fine-grained control over individual layers color with custom fills
-
SwiftUI automatically chooses the correct symbol variant to use based on the context, for example a symbol used in the tabbar will use the
.fill
variant.
New view allowing immediate-mode drawing similar to drawRect
from UIKit or AppKit:
Canvas { context, size in
let metrics = gridMetrics(in: size)
for (index, symbol) in symbols.enumerated() {
let rect = metrics[index]
let (sRect, opacity) = rect.fishEyeTransform(around: focalPoint)
context.opacity = opacity
let image = context.resolve(symbol.image)
context.draw(image, in: sRect.fit(image.size))
}
}
We can use TimelineView
to make our canvas update over time.
New modifiers that automatically redact sensitive data when the user is no longer authenticated (for when the phone is locked or similar)
Image(systemName: favoriteSymbol)
.font(.title2)
.privacySensitive(true)
New blur/vibrancy effects:
struct ColorList: View {
var body: some View {
ZStack {
...
materialOverlay
}
}
var materialOverlay: some View {
VStack {
Text("Symbol Browser")
.font(.largeTitle.bold())
Text("\(symbols.count) symbols 🎉")
.foregroundStyle(.secondary)
.font(.title2.bold())
}
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16.0))
}
}
We can now preview screens in different orientations:
struct ColorList_Previews: PreviewProvider {
static var previews: some View {
ColorList()
.previewInterfaceOrientation(.portrait)
ColorList()
.previewInterfaceOrientation(.landscapeLeft)
}
}
- markdown support
- AttributedString
- restrict dynamic type size of a text/view via new dynamicTypeSize(_:) view modifier
- make text selectable or not via textSelection(_:) view modifier (macOS only)
- new powerful formatters
- support for prompts, separate from its label, to let users know what kind of content a field is expecting. In macOS, the prompt will be used as the placeholder text.
onSubmit(_:)
view modifier to detect when the user submits the text (this replaces the previousTextField
'sonCommit
parametersubmitLabel(_:)
view modifier to customize the return key action, and to help give users a hint of what kind of action will occur when submitting a field
struct ContentView: View {
@State private var activity: Activity = .sample
@State private var newAttendee = PersonNameComponents()
var body: some View {
TextField("New Person", value: $newAttendee,
format: .name(style: .medium)
)
.onSubmit {
activity.append(Person(newAttendee))
newAttendee = PersonNameComponents()
}
.submitLabel(.done)
}
}
- keyboard toolbar support via the usual
toolbar(_:)
view modifier with new.keyboard
placement
struct ContentView: View {
@State private var activity: Activity = .sample
@FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Name", text: $activity.name, prompt: Text("New Activity"))
TextField("Location", text: $activity.location)
DatePicker("Date", selection: $activity.date)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button(action: selectPreviousField) {
Label("Previous", systemImage: "chevron.up")
}
.disabled(!hasPreviousField)
Button(action: selectNextField) {
Label("Next", systemImage: "chevron.down")
}
.disabled(!hasNextField)
}
}
}
private func selectPreviousField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue - 1)!
}
}
private var hasPreviousField: Bool {
if let currentFocusedField = focusedField {
return currentFocusedField.rawValue > 0
} else {
return false
}
}
private func selectNextField() {
focusedField = focusedField.map {
Field(rawValue: $0.rawValue + 1)!
}
}
private var hasNextField: Bool {
if let currentFocusedField = focusedField {
return currentFocusedField.rawValue < Field.allCases.count
} else {
return false
}
}
}
- textfield focus control via
@FocusState
property wrapper:
struct ContentView: View {
@State private var activity: Activity = .sample
@State private var newAttendee = PersonNameComponents()
@FocusState private var addAttendeeIsFocused: Bool = false
var body: some View {
VStack(alignment: .leading) {
TextField("New Person", value: $newAttendee, format: .name(style: .medium))
.focused($addAttendeeIsFocused)
ControlGroup {
Button {
addAttendeeIsFocused = true
} label: {
Label("Add Attendee", systemImage: "plus")
}
}
}
}
}
- New bordered style (
Button("Add") {}.buttonStyle(.bordered)
), which supports tinting via the.tint
view modifier - new
controlSize(_:)
view modifier for different buttons appearances - new
controlProminence(_:)
to highlight importance of each button
struct ContentView: View {
var body: some View {
VStack {
Button(action: addToJar) {
Text("Add to Jar").frame(maxWidth: 300)
}
.controlProminence(.increased)
.keyboardShortcut(.defaultAction)
Button(action: addToWatchlist) {
Text("Add to Watchlist").frame(maxWidth: 300)
}
.tint(.accentColor)
}
.buttonStyle(.bordered)
.controlSize(.large)
}
private func addToJar() {}
private func addToWatchlist() {}
}
- New Button roles to give each button additional semantics, which SwiftUI uses to display the button accordingly:
struct ContentView: View {
var entry: ButtonEntry = .sample
var body: some View {
ButtonEntryCell(entry)
.contextMenu {
Section {
Button("Open") {
// ...
}
// This button will have red tint as it's destructive
Button("Delete...", role: .destructive) {
// ...
}
}
}
}
- Buttons confirmation dialogs via
confirmationDialog
view modifier:
struct ContentView: View {
var entry: ButtonEntry = .sample
@State private var showConfirmation: Bool = false
var body: some View {
ButtonEntryCell(entry)
.contextMenu {
Section {
Button("Open") {
// ...
}
Button("Delete...", role: .destructive) {
showConfirmation = true
// ...
}
}
}
.confirmationDialog(
"Are you sure you want to delete \(entry.name)?",
isPresented: $showConfirmation
) {
Button("Delete", role: .destructive) {
// delete the entry
}
} message: {
Text("Deleting \(entry.name) will remove it from all of your jars.")
}
}
}
More flexibility and new modifiers to control primary and secondary actions:
struct ContentView: View {
var buttonEntry: ButtonEntry = .sample
@StateObject private var jarStore = JarStore()
var body: some View {
Menu("Add") {
ForEach(jarStore.allJars) { jar in
Button("Add to \(jar.name)") {
jarStore.add(buttonEntry, to: jar)
}
}
} primaryAction: {
jarStore.addToDefaultJar(buttonEntry)
}
.menuStyle(BorderedButtonMenuStyle())
.
}
}
New view used to gather controls together (the system will display the controls at the right place with correct spacing etc):
ControlGroup {
Button(action: archive) {
Label("Archive", systemImage: "archiveBox")
}
Button(action: delete) {
Label("Delete", systemName: "trash")
}
}