• SwiftUI in more places

    • headset부터 watch, 위젯과 크로스플랫폼 통합까지

    • 공간 컴퓨팅에서도 SwiftUI가 사용되고 이를 위한 3D 기능이 추가되었다.

      • volume - 몰입감 있는 공간
      • 3D 제스쳐, 효과, 레이아웃
      • RealityKit과의 통합
    • 홈 뷰부터 TV, Safari, Freeform, Keynote의 immersive rehearsal 기능 등이 모두 swiftUI로 구성되어 있다.

    • 똑같이 WindowGroup을 쓰면 2D Window를 그려준다.

      스크린샷 2023-06-13 오후 2.47.57.png

    • 그 안에서 똑같이 SwiftUI 컨테이너를 써주면 된다.

      스크린샷 2023-06-13 오후 2.48.37.png

      스크린샷 2023-06-13 오후 2.49.19.png

    • 여기에 더해서 Scene을 3D로 만들 수도 있다.

      스크린샷 2023-06-13 오후 2.50.27.png

    • 정적 모델로 채우기 - Model3D

      스크린샷 2023-06-13 오후 2.51.30.png

    • 동적으로 상호작용이 가능한 뷰 - RealityView

      스크린샷 2023-06-13 오후 2.51.46.png

    • 새로운 Scene인 ImmersiveSpace 추가

      • 다른 앱들을 치워서 사용자가 해당 앱에 몰입하게 해준다.

      스크린샷 2023-06-13 오후 2.53.15.png

    • watchOS 10 리뉴얼

      • 기존 뷰 컨테이너 리뉴얼

        스크린샷 2023-06-13 오후 2.59.28.png

      • 신규 API 추가

        스크린샷 2023-06-13 오후 3.00.23.png

      • toolbar 아이템 위치 조정

        스크린샷 2023-06-13 오후 3.02.32.png

      • 일부 API가 WatchOS에서도 사용 가능하게 됨.

        • DatePicker

          스크린샷 2023-06-13 오후 3.03.25.png

        • Selection in List

          스크린샷 2023-06-13 오후 3.03.37.png

      • Widget

        • watchOS에 Smart Stack으로 위젯을 빠르게 볼 수있음

        • iPadOS에 Lock Screen Widget 추가

        • iPhone의 AOD를 활용한 Standby mode

        • macOS의 desktop widget

        • interactive 지원

        • Widget을 위한 preview매크로

          #Preview(as: .systemSmall) {
          	CaffeineTrackerWidget()
          } timeline : {
          	CaffeineLogEntry.log1
          	CaffeineLogEntry.log2
          	CaffeineLogEntry.log3
          	CaffeineLogEntry.log4
          }
          
      • Apple 프레임워크들의 SwiftUI 지원 추가

        스크린샷 2023-06-13 오후 3.14.29.png

        • MapKit

          스크린샷 2023-06-13 오후 3.16.12.png

        • Swift Chart

          • 차트 스크롤링

            import SwiftUI
            import Charts
            
            struct ScrollingChart_Snippet: View {
                @State private var scrollPosition = SalesData.last365Days.first!
                @State private var selection: SalesData?
            
                var body: some View {
                    VStack(alignment: .leading) {
                        VStack(alignment: .leading) {
                            Text("""
                                Scrolled to: \\
                                \\(scrollPosition.day,
                                    format: .dateTime.day().month().year())
                                """)
                            Text("""
                                Selected: \\
                                \\(selection?.day ?? .now,
                                    format: .dateTime.day().month().year())
                                """)
                            .opacity(selection != nil ? 1.0 : 0.0)
                        }
                        .padding([.leading, .trailing])
                        Chart {
                            ForEach(SalesData.last365Days, id: \\.day) {
                                BarMark(
                                    x: .value("Day", $0.day, unit: .day),
                                    y: .value("Sales", $0.sales))
                            }
                            .foregroundStyle(.blue)
                        }
                        .chartScrollableAxes(.horizontal)
                        .chartXVisibleDomain(length: 3600 * 24 * 30)
                        .chartScrollPosition(x: $scrollPosition)
                        .chartXSelection(value: $selection)
                    }
                }
            }
            
            struct SalesData: Plottable {
                var day: Date
                var sales: Int
            
                var primitivePlottable: Date { day }
            
                init?(primitivePlottable: Date) {
                    self.day = primitivePlottable
                    self.sales = 0
                }
            
                init(day: Date, sales: Int) {
                    self.day = day
                    self.sales = sales
                }
            
                static let last365Days: [SalesData] = buildSalesData()
            
                private static func buildSalesData() -> [SalesData] {
                    var result: [SalesData] = []
                    var date = Date.now
                    for _ in 0..<365 {
                        result.append(SalesData(day: date, sales: Int.random(in: 150...250)))
                        date = Calendar.current.date(
                            byAdding: .day, value: -1, to: date)!
                    }
                    return result.reversed()
                }
            }
            
            #Preview {
                ScrollingChart_Snippet()
            }
            
          • selection 지원

          • 도넛 차트와 파이 차트 지원(SectorMark)

            import SwiftUI
            import Charts
            
            struct DonutChart_Snippet: View {
                var sales = Bagel.salesData
            
                var body: some View {
                    NavigationStack {
                        Chart(sales, id: \\.name) { element in
                            SectorMark(
                                angle: .value("Sales", element.sales),
                                innerRadius: .ratio(0.6),
                                angularInset: 1.5)
                            .cornerRadius(5)
                            .foregroundStyle(by: .value("Name", element.name))
                        }
                        .padding()
                        .navigationTitle("Bagel Sales")
                        .toolbarTitleDisplayMode(.inlineLarge)
                    }
                }
            }
            
            struct Bagel {
                var name: String
                var sales: Int
            
                static var salesData: [Bagel] = buildSalesData()
            
                static func buildSalesData() -> [Bagel] {
                    [
                        Bagel(name: "Blueberry", sales: 60),
                        Bagel(name: "Everything", sales: 120),
                        Bagel(name: "Choc. Chip", sales: 40),
                        Bagel(name: "Cin. Raisin", sales: 100),
                        Bagel(name: "Plain", sales: 140),
                        Bagel(name: "Onion", sales: 70),
                        Bagel(name: "Sesame Seed", sales: 110),
                    ]
                }
            }
            
            #Preview {
                DonutChart_Snippet()
            }
            
        • StoreKit

          import SwiftUI
          import StoreKit
          
          struct SubscriptionStore_Snippet {
              var body: some View {
                  SubscriptionStoreView(groupID: passGroupID) {
                      PassMarketingContent()
                          .lightMarketingContentStyle()
                          .containerBackground(for: .subscriptionStoreFullHeight) {
                              SkyBackground()
                          }
                  }
                  .backgroundStyle(.clear)
                  .subscriptionStoreButtonLabel(.multiline)
                  .subscriptionStorePickerItemBackground(.thinMaterial)
                  .storeButton(.visible, for: .redeemCode)
              }
          }
          
  • Simplify data flow

    • Observable 매크로

      • 일일이 Published를 할 필요가 없다.

        import Foundation
        import SwiftUI
        
        @Observable
        class Dog: Identifiable {
            var id = UUID()
            var name = ""
            var age = 1
            var breed = DogBreed.mutt
            var owner: Person? = nil
        }
        
        class Person: Identifiable {
            var id = UUID()
            var name = ""
        }
        
        enum DogBreed {
            case mutt
        }
        
      • 사용할 때도 프로퍼티 래퍼가 필요없고, 의존성을 자동으로 설정한다.

      • 실제로 읽는 프로퍼티만 재평가하기 때문에 중간 뷰가 불필요하게 reevaluation되지 않는다.

        import Foundation
        import SwiftUI
        
        struct DogCard: View {
            var dog: Dog
            
            var body: some View {
                DogImage(dog: dog)
                    .overlay(alignment: .bottom) {
                        HStack {
                            Text(dog.name)
                            Spacer()
                            Image(systemName: "heart")
                                .symbolVariant(dog.isFavorite ? .fill : .none)
                        }
                        .font(.headline)
                        .padding(.horizontal, 22)
                        .padding(.vertical, 12)
                        .background(.thinMaterial)
                    }
                    .clipShape(.rect(cornerRadius: 16))
            }
            
            struct DogImage: View {
                var dog: Dog
        
                var body: some View {
                    Rectangle()
                        .fill(Color.green)
                        .frame(width: 400, height: 400)
                }
            }
        
            @Observable
            class Dog: Identifiable {
                var id = UUID()
                var name = ""
                var isFavorite = false
            }
        }
        
      • 기존에 ObservableObject와 함께 동작하던 PropertyWrapper들은 모두 필요없다.

        • State와 Binding, Environment만 남는다. Observable과 함께 잘 동작한다.

          // State
          import Foundation
          import SwiftUI
          
          struct AddSightingView: View {
              @State private var model = DogDetails()
          
              var body: some View {
                  Form {
                      Section {
                          TextField("Name", text: $model.dogName)
                          DogBreedPicker(selection: $model.dogBreed)
                      }
                      Section {
                          TextField("Location", text: $model.location)
                      }
                  }
              }
          
              struct DogBreedPicker: View {
                  @Binding var selection: DogBreed
          
                  var body: some View {
                      Picker("Breed", selection: $selection) {
                          ForEach(DogBreed.allCases) {
                              Text($0.rawValue.capitalized)
                                  .tag($0.id)
                          }
                      }
                  }
              }
          
              @Observable
              class DogDetails {
                  var dogName = ""
                  var dogBreed = DogBreed.mutt
                  var location = ""
              }
          
              enum DogBreed: String, CaseIterable, Identifiable {
                  case mutt
                  case husky
                  case beagle
          
                  var id: Self { self }
              }
          }
          
          #Preview {
              AddSightingView()
          }
          
          // Environment
          import SwiftUI
          
          @main
          private struct WhatsNew2023: App {
              @State private var currentUser: User?
              
              var body: some Scene {
                  WindowGroup {
                      ContentView()
                          .environment(currentUser)
                  }
              }
              
              struct ContentView: View {
                  var body: some View {
                      Color.clear
                  }
              }
          
              struct ProfileView: View {
                  @Environment(User.self) private var currentUser: User?
          
                  var body: some View {
                      if let currentUser {
                          UserDetails(user: currentUser)
                      } else {
                          Button("Log In") { }
                      }
                  }
              }
          
              struct UserDetails: View {
                  var user: User
          
                  var body: some View {
                      Text("Hello, \\(user.name)")
                  }
              }
          
              @Observable
              class User: Identifiable {
                  var id = UUID()
                  var name = ""
              }
          }
          
    • SwiftData

      • Persist 기능을 더해준다.

      • Observable 매크로를 Model로 바꿔주면 된다

      • Query 매크로를 통해서 데이터를 가져올 수 있다.

        import Foundation
        import SwiftUI
        import SwiftData
        
        struct RecentDogsView: View {
            @Query(sort: \\.dateSpotted) private var dogs: [Dog]
        
            var body: some View {
                ScrollView(.vertical) {
                    LazyVStack {
                        ForEach(dogs) { dog in
                            DogCard(dog: dog)
                        }
                    }
                }
            }
        
            struct DogCard: View {
                var dog: Dog
                
                var body: some View {
                    DogImage(dog: dog)
                        .overlay(alignment: .bottom) {
                            HStack {
                                Text(dog.name)
                                Spacer()
                                Image(systemName: "heart")
                                    .symbolVariant(dog.isFavorite ? .fill : .none)
                            }
                            .font(.headline)
                            .padding(.horizontal, 22)
                            .padding(.vertical, 12)
                            .background(.thinMaterial)
                        }
                        .clipShape(.rect(cornerRadius: 16))
                }
            }
        
            struct DogImage: View {
                var dog: Dog
        
                var body: some View {
                    Rectangle()
                        .fill(Color.green)
                        .frame(width: 400, height: 400)
                }
            }
        
            @Model
            class Dog: Identifiable {
                var name = ""
                var isFavorite = false
                var dateSpotted = Date.now
            }
        }
        
        #Preview {
            RecentDogsView()
        }
        
        • Document기반 앱에서도 네이티브로 지원

          import SwiftUI
          import SwiftData
          import UniformTypeIdentifiers
          
          @main
          private struct WhatsNew2023: App {
              var body: some Scene {
                  DocumentGroup(editing: DogTag.self, contentType: .dogTag) {
                      ContentView()
                  }
              }
              
              struct ContentView: View {
                  var body: some View {
                      Color.clear
                  }
              }
          
              @Model
              class DogTag {
                  var text = ""
              }
          }
          
          extension UTType {
              static var dogTag: UTType {
                  UTType(exportedAs: "com.apple.SwiftUI.dogTag")
              }
          }
          
    • Inspector 지원

      • 디테일한 옵션들을 볼 수 있는 영역
      • Mac과 iPad의 regular 사이즈 클래스에서는 오른쪽 영역, compact한 상태에서는 sheet로 뜬다.
    • Dialog용 modifier 추가

      • file Export

        import Foundation
        import SwiftUI
        import UniformTypeIdentifiers
        
        struct ExportDialogCustomization: View {
            @State private var isExporterPresented = true
            @State private var selectedItem = ""
            
            var body: some View {
                Color.clear
                    .fileExporter(
                        isPresented: $isExporterPresented, item: selectedItem,
                        contentTypes: [.plainText], defaultFilename: "ExportedData.txt")
                    { result in
                        handleDataExport(result: result)
                    }
                    .fileExporterFilenameLabel("Export Data")
                    .fileDialogConfirmationLabel("Export Data")
            }
        
            func handleDataExport(result: Result<URL, Error>) {
            }
        
            struct Data: Codable, Transferable {
                static var transferRepresentation: some TransferRepresentation {
                    CodableRepresentation(contentType: .plainText)
                }
        
                var text = "Exported Data"
            }
        }
        
      • dialog

        import Foundation
        import SwiftUI
        import UniformTypeIdentifiers
        
        struct ConfirmationDialogCustomization: View {
            @State private var showDeleteDialog = false
            @AppStorage("dialogIsSuppressed") private var dialogIsSuppressed = false
        
            var body: some View {
                Button("Show Dialog") {
                    if !dialogIsSuppressed {
                        showDeleteDialog = true
                    }
                }
                .confirmationDialog(
                    "Are you sure you want to delete the selected dog tag?",
                    isPresented: $showDeleteDialog)
                {
                    Button("Delete dog tag", role: .destructive) { }
        
                    HelpLink { ... }
                }
                .dialogSeverity(.critical)
                .dialogSuppressionToggle(isSuppressed: $dialogIsSuppressed)
            }
        }
        
      • table

        • 열 정렬 및 숨기기 지원

        • SceneStorage와 결합하면 앱을 껏다 켜도 유지된다.

          import SwiftUI
          
          struct DogSightingsTable: View {
              private var dogSightings: [DogSighting] = (1..<50).map {
                  .init(
                      name: "Sighting \\($0)",
                      date: .now + Double((Int.random(in: -5..<5) * 86400)))
              }
          
              @SceneStorage("columnCustomization")
              private var columnCustomization: TableColumnCustomization<DogSighting>
              @State private var selectedSighting: DogSighting.ID?
              
              var body: some View {
                  Table(
                      dogSightings, selection: $selectedSighting,
                      columnCustomization: $columnCustomization)
                  {
                      TableColumn("Dog Name", value: \\.name)
                          .customizationID("name")
                      
                      TableColumn("Date") {
                          Text($0.date, style: .date)
                      }
                      .customizationID("date")
                  }
              }
              
              struct DogSighting: Identifiable {
                  var id = UUID()
                  var name: String
                  var date: Date
              }
          }
          
        • DisclosureTableRow: 다른 Row를 가지고 있는 Row

          import SwiftUI
          
          struct DogGenealogyTable: View {
              private static let dogToys = ["🦴", "🧸", "👟", "🎾", "🥏"]
          
              private var dogs: [DogGenealogy] = (1..<10).map {
                  .init(
                      name: "Parent \\($0)", age: Int.random(in: 8..<12) * 7,
                      favoriteToy: dogToys[Int.random(in: 0..<5)],
                      children: (1..<10).map {
                          .init(
                              name: "Child \\($0)", age: Int.random(in: 1..<5) * 7,
                              favoriteToy: dogToys[Int.random(in: 0..<5)])
                      }
                  )
              }
          
              var body: some View {
                  Table(of: DogGenealogy.self) {
                      TableColumn("Dog Name", value: \\.name)
                      TableColumn("Age (Dog Years)") {
                          Text($0.age, format: .number)
                      }
                      TableColumn("Favorite Toy", value: \\.favoriteToy)
                  } rows: {
                      ForEach(dogs) { dog in
                          DisclosureTableRow(dog) {
                              ForEach(dog.children) { child in
                                  TableRow(child)
                              }
                          }
                      }
                  }
              }
          
              struct DogGenealogy: Identifiable {
                  var id = UUID()
                  var name: String
                  var age: Int
                  var favoriteToy: String
                  var children: [DogGenealogy] = []
              }
          }
          
        • 코드로 열 펼치기 기능 지원

          import SwiftUI
          
          struct ExpandableSectionsView: View {
              @State private var selection: Int?
          
              var body: some View {
                  NavigationSplitView {
                      Sidebar(selection: $selection)
                  } detail: {
                      Detail(selection: selection)
                  }
              }
          
              struct Sidebar: View {
                  @Binding var selection: Int?
                  @State private var isSection1Expanded = true
                  @State private var isSection2Expanded = false
          
                  var body: some View {
                      List(selection: $selection) {
                          Section("First Section", isExpanded: $isSection1Expanded) {
                              ForEach(1..<6, id: \\.self) {
                                  Text("Item \\($0)")
                              }
                          }
                          Section("Second Section", isExpanded: $isSection2Expanded) {
                              ForEach(6..<11, id: \\.self) {
                                  Text("Item \\($0)")
                              }
                          }
                      }
                  }
              }
          
              struct Detail: View {
                  var selection: Int?
          
                  var body: some View {
                      Text(selection.map { "Selection: \\($0)" } ?? "No Selection")
                  }
              }
          }
          
        • 추가 스타일링

  • Extraordinary animations

  • Enhanced interactions

  • HDR 이미지 지원

  • accessibilityZoomAction

  • Menu 강화