Fresh apps
탭뷰 개선
사이드바와 탭뷰간 자유롭게 전환
사용자는 자유롭게 사이드바 및 탭바 커스텀 가능
새로운 타입 안전 구문을 통해서 빌드 타임에 에러를 잡기 쉽게 되었다.
import SwiftUI
struct KaraokeTabView: View {
var body: some View {
TabView {
Tab("Parties", image: "party.popper") {
PartiesView(parties: Party.all)
}
Tab("Planning", image: "pencil.and.list.clipboard") {
PlanningView()
}
Tab("Attendance", image: "person.3") {
AttendanceView()
}
Tab("Song List", image: "music.note.list") {
SongListView()
}
}
}
}
탭뷰 사이드바 간 전환은 스타일만 지정해주면 된다.
var body: some View {
TabView {
Tab("Parties", image: "party.popper") {
PartiesView(parties: Party.all)
}
}
.tabViewStyle(.sidbarAdaptable)
}
커스터마이제이션도 가능하고, 프로그래밍적으로 하는 것도 가능하다.
@State var customization = TabViewCustomization()
var body: some View {
TabView {
Tab("Parties", image: "party.popper") {
PartiesView(parties: Party.all)
}
.customizationID("karaoke.tab.parties")
}
.tabViewStyle(.sidbarAdaptable)
.tabViewCustomization($customization)
}
새로운 사이드바는 tvOS에서도 동작하고, macOS에서도 사이드바 혹은 툴바의 세그멘테이션 컨트롤로써 동작한다.
Improve your tab and sidebar experience on iPad
시트 사이즈 표현이 단순화되고 모든 플랫폼에서 통합됨
presentationSizing modifier 사용
form, page, 커스텀 사이즈 지원
struct AllPartiesView: View {
@State var showAddSheet: Bool = true
var parties: [Party] = []
var body: some View {
PartiesGridView(parties: parties, showAddSheet: $showAddSheet)
.sheet(isPresented: $showAddSheet) {
AddPartyView()
.presentationSizing(.form)
}
}
}
Zoom Transition
import SwiftUI
struct PartyView: View {
var party: Party
@Namespace() var namespace
var body: some View {
NavigationLink {
PartyDetailView(party: party)
.navigationTransition(.zoom(
sourceID: party.id, in: namespace))
} label: {
Text("Party!")
}
.matchedTransitionSource(id: party.id, in: namespace)
}
}
struct PartyDetailView: View {
var party: Party
var body: some View {
Text("PartyDetailView")
}
}
제어센터나 잠금 화면에 크기 변경이 가능한 커스텀 컨트롤을 추가할 수 있다.
버튼, 토글 등
액션 버튼으로도 사용할 수 있다.
AppIntent와 함께 사용할 수 있는 새로운 Widget이다.
Access your app’s controls across the system 세션 참조
import WidgetKit
import SwiftUI
struct StartPartyControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.karaoke_start_party"
) {
ControlWidgetButton(action: StartPartyIntent()) {
Label("Start the Party!", systemImage: "music.mic")
Text(PartyManager.shared.nextParty.name)
}
}
}
}
Swift Chart 개선
Swift Charts: Vectorized and funcion plots 세션 참조
struct AttendanceView: View {
var body: some View {
Chart {
LinePlot(x: "Parties", y: "Guests") { x in
pow(x, 2)
}
.foregroundStyle(.purple)
}
.chartXScale(domain: 1...10)
.chartYScale(domain: 1...100)
}
}
동적 컬럼 지원
var body: some View {
Table(Self.guestData) {
// A static column for the name
TableColumn("Name", value: \\.name)
TableColumnForEach(Self.partyData) { party in
TableColumn(party.name) { guest in
Text(guest.songsSung[party.id] ?? 0, format: .number)
}
}
}
}
Mesh Gradient 지원 추가
struct MyMesh: View {
var body: some View {
MeshGradient(
width: 3,
height: 3,
points: [
.init(0, 0), .init(0.5, 0), .init(1, 0),
.init(0, 0.5), .init(0.3, 0.5), .init(1, 0.5),
.init(0, 1), .init(0.5, 1), .init(1, 1)
],
colors: [
.red, .purple, .indigo,
.orange, .cyan, .blue,
.yellow, .green, .mint
]
)
}
}
Document based app 런치 씬 지원
DocumentGroupLaunchScene("Your Lyrics") {
NewDocumentButton()
Button("New Parody from Existing Song") {
// Do something!
}
} background: {
PinkPurpleGradient()
} backgroundAccessoryView: { geometry in
MusicNotesAccessoryView(geometry: geometry)
.symbolEffect(.wiggle(.rotational.continuous())) // 새로운 symbol effect도 추가
} overlayAccessoryView: { geometry in
MicrophoneAccessoryView(geometry: geometry)
}
Harnessing the platform
Windowing
window style 지정
window level 지정
defaultWindowPlacement: 윈도우의 기본 위치 지정
Window("Lyric Preview", id: "lyricPreview") {
LyricPreview()
}
.windowStyle(.plain) // 윈도우 chrome제거
.windowLevel(.floating) // 다른 윈도우보다 위에 뜨도록
.defaultWindowPlacement { content, context in // 기본 윈도우 위치 지정
let displayBounds = context.defaultDisplay.visibleRect
let contentSize = content.sizeThatFits(.unspecified)
return topPreviewPlacement(size: contentSize, bounds: displayBounds)
}
}
windowDragGesture 지원 추가
Text(currentLyric)
.background(.thinMaterial, in: .capsule)
.gesture(WindowDragGesture())
utility window 등의 새로운 씬 타입 추가
pushWindow environment action
기존 윈도우와 같은 크기의 윈도우를 띄워서 기존 윈도우를 덮게 만든다.
struct EditorView: View {
@Environment(\\.pushWindow) private var pushWindow
var body: some View {
Button("Play", systemImage: "play.fill") {
pushWindow(id: "lyric-preview")
}
}
}
Input methods
visionOS에서는 시야가 갈 때, 손가락을 근처에둘 때, 포인터를 옮길 때 등의 경우에 프라이버시를 보호하면서도 뷰가 반응하도록 할 수 있다
struct ProfileButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(.thinMaterial)
.hoverEffect(.highlight)
.clipShape(.capsule)
.hoverEffect { effect, isActive, _ in
effect.scaleEffect(isActive ? 1.05 : 1.0)
}
}
}
macOS 메인 메뉴에서 modifier키를 누르면 숨겨져있던 메뉴를 띄울 수 있다.
Button("Preview Lyrics in Window") {
// show preview in window
}
.modifierKeyAlternate(.option) {
Button("Preview Lyrics in Full Screen") {
// show preview in full screen
}
}
.keyboardShortcut("p", modifiers: [.shift, .command])
뷰 자체도 modifier 키에 반응하도록 할 수 있다.
var body: some View {
LyricLine()
.overlay(alignment: .top) {
if showBouncingBallAlignment {
// Show bouncing ball alignment guide
}
}
.onModifierKeysChanged(mask: .option) {
showBouncingBallAlignment = !$1.isEmpty
}
}
시스템 포인터 스타일 커스텀
ForEach(resizeAnchors) { anchor in
ResizeHandle(anchor: anchor)
.pointerStyle(.frameResize(position: anchor.position))
}
Apple Pencil Pro 지원 추가
squeeze 관련 지원 추가
@Environment(\\.preferredPencilSqueezeAction) var preferredAction
var body: some View {
LyricsEditorView()
.onPencilSqueeze { phase in
if preferredAction == .showContextualPalette, case let .ended(value) = phase {
if let anchorPoint = value.hoverPose?.anchor {
lyricDoodlePaletteAnchor = .point(anchorPoint)
}
lyricDoodlePalettePresented = true
}
}
Widgets and Live Activities
Live Activity가 추가 작업없이 WatchOS도 지원.
작은 뷰에서 디자인을 다듬기 위한 지원 추가
WatchOS만의 제스처 지원도 추가
struct KaraokeLyricActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(
for: KaraokeLiveActivityAttributes.self
) { context in
HStack {
LyricView()
Button("Next", intent: LyricIntent(lyrics: lyrics))
.handGestureShortcut(.primaryAction)
}
}
}
.supplementalActivityFamilies([.small, .medium])
}
struct LyricView: View {
@Environment(\\.activityFamily) private var activityFamily
var context: ActivityViewContext<KaraokeLiveAttributes>
var body: some View {
switch activityFamily {
case .small: WatchLyricView(context)
case .medium: MultiLineLyricView(context)
}
}
}
새로운 date 포맷 추가
Text(.currentDate, format: .reference(to: nextSongDate))
relevanceContext를 통해서 시스템이 스마트 스택등에서 적절한 때에 보여주도록 할 수 있다.
func relevance() async -> WidgetRelevances<Void> {
let dateEntries = nextKaraokeDates.map {
WidgetRelevanceEntry(context: .date($0))
}
let locationEntries = favoriteKaraokeVenues.map {
WidgetRelevanceEntry(context: .location($0))
}
return WidgetRelevance(dateEntries + locationEntries)
}
Framework foundations
ForEach를 통한 커스텀 컨테이너 지원
struct DisplayBoard<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
DisplayBoardCardLayout {
ForEach(subviewOf: content) { subview in
CardView {
subview
}
}
}
.background { BoardBackgroundView() }
}
}
SwiftUI의 내장 컨테이너인 List나 Picker처럼 사용 가능
DisplayBoard {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body Like View")
ForEach(songsFromSam) { song in
Text(song.title)
}
}
DisplayBoard {
Section("Matt's Favorites") {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body Like View")
.displayBoardCardRejected(true)
}
Section("Sam's Favorites") {
ForEach(songsFromSam) { song in
Text(song.title)
}
}
}
Demystify SwiftUI Containers 세션 참조
Crafting experiences