• 이 영상은 1번은 라이브로 볼 필요가 있습니다.

  • 올해 SwiftUI 업데이트 요약

    스크린샷 2022-06-12 오후 10.39.31.png

  • SwiftUI로 만든 시스템 요소들

    스크린샷 2022-06-12 오후 10.40.41.png

  • Swift Chats

    • 상태기반으로 차트를 그리는 API

    • SwiftUI와 동일한 패러다임이 적용되었고, SwiftUI와 함께 사용 가능

    • 접근성 대응 완벽 지원

      var body: some View {
      	Chart(parthTaskRemaining) { task in
      		BarMark(
      			x: .value("Date", task.date, unit: .day),
      			y: .value("Tasks Remaining", task.remainingCount)
      		)
      	}.padding()
      }
      
      var body: some View {
          Chart(partyTasksRemaining) {
              LineMark(
                  x: .value("Date", $0.date, unit: .day),
                  y: .value("Tasks Remaining", $0.remainingCount)
              )
              .foregroundStyle(by: .value("Category", $0.category))
          }
          .padding()
      }
      
      var body: some View {
          Chart(partyTasksRemaining) {
              LineMark(
                  x: .value("Date", $0.date, unit: .day),
                  y: .value("Tasks Remaining", $0.remainingCount)
              )
              .foregroundStyle(by: .value("Category", $0.category))
              .symbol(by: .value("Category", $0.category))
          }
          .padding()
      }
      
      var body: some View {
          Chart {
              ForEach(partyTasksRemaining) { task in
                  LineMark(
                      x: .value("Date", task.date, unit: .day),
                      y: .value("Tasks Remaining", task.remainingCount)
                  )
                  .foregroundStyle(by: .value("Category", task.category))
                  .symbol(by: .value("Category", task.category))
                  .annotation(position: .leading) {
                      Text("\\(task.category.emoji)")
                  }
              }
      
              RuleMark(y: .value("Value", 5))
                  .foregroundStyle(.red)
                  .lineStyle(StrokeStyle(lineWidth: 2.0, dash: [4, 5]))
                  .annotation(position: .top, alignment: .trailing) {
                      VStack(alignment: .trailing) {
                          Text("Today's Goal")
                          Text("Status: ✔︎")
                      }
                      .font(.caption)
                      .foregroundColor(.gray)
                      .padding(.trailing, 2)
                  }
          }
      }
      
  • Navigation and windows

    • navigation stack

      • 기존 NavigationLink와도 사용 가능

        NavigationStack {
        	List(foodItems) { item in
        		NavigationLink {
        			FoodDetailView(item: item)
        		} label: {
        			Label(item.title, image: item.iconName)
        		}
        	}
        }
        
      • 데이터 기반 네비게이션 스택 → 타입을 이용해서 스택을 쌓기

        NavigationStack {
        	List(foodItems) { item in
        		NavigationLink(value: item) {
        			Label(item.title, image: item.iconName)
        		}
        	}
        	.navigationTitle("Party Food")
        	.navigationDestination(for: FoodItem.self) { item in
        			FoodDetailView(item: item)
        	}
        }
        
      • 스택 조작

        @State private var selectedFoodItems: [FoodItem] = []
        
        var body: some View {
        	NavigationStack(path: $selectedFoodItems) {
        		List(foodItems) { item in
        			NavigationLink(value: item) {
        				Label(item.title, image: item.iconName)
        			}
        		}
        		.navigationTitle("Party Food")
        		.navigationDestination(for: FoodItem.self) { item in
        			FoodDetailView(item: item, path: $selectedFoodItems)
        		}
        	}
        }
        
        // .. FoodDetailView.body
         Button("Back to First Item") {
        		selectedFoodItems.removeSubrange(1...)
        }
        
    • split views

      • 기본 예제

        • 화면이 작아졌을 때는 자동으로 stack으로 변한다.
        @State private var selectedTask: PartyTask?
        
            var body: some View {
                NavigationSplitView {
                    List(PartyTask.allCases, selection: $selectedTask) { task in
                        NavigationLink(value: task) {
                            TaskLabel(task: task)
                        }
        						    .listItemTint(task.color)
                    }
                } detail: {
                    selectedTask.flatMap { $0.color } ?? .white
                }
            }
        
      • NavigationStack과의 합성도 가능

        struct PartyPlannerHome: View {
            @State private var selectedTask: PartyTask?
        
            var body: some View {
                NavigationSplitView {
                    List(PartyTask.allCases, selection: $selectedTask) { task in
                        NavigationLink(value: task) {
                            TaskLabel(task: task)
                        }
                        .listItemTint(task.color)
                    }
                } detail: {
                    if case .food = selectedTask {
                        FoodsListView() // 내부에 NavigationStack을 가지고 있음
                    } else {
                        selectedTask.flatMap { $0.color } ?? .white
                    }
                }
            }
        }
        
    • multi window

      • 메인 인터페이스인 windowGroup와는 다른 개별 윈도우를 정의할 수 있게 됨

        @main
        struct PartyPlanner: App {
            var body: some Scene {
                WindowGroup("Party Planner") {
                    PartyPlannerHome()
                }
        
                Window("Party Budget", id: "budget") {
                    Text("Budget View")
                }
                .keyboardShortcut("0")
            }
        }
        
      • 새로운 window 열기

        struct DetailView: View {
            @Environment(\\.openWindow) var openWindow
        
            var body: some View {
                Text("Detail View")
                    .toolbar {
                        Button {
                            openWindow(id: "budget")
                        } label: {
                            Image(systemName: "dollarsign")
                        }
                    }
            }
        }
        
      • window 커스텀

        • 사용자가 수동으로 변경했을 경우, 이를 자동으로 기억함.
        @main
        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)
            }
        }
        
    • sheet 높이 조정 modifier

      struct PartyPlannerHome: View {
          @State private var selectedTask: PartyTask?
          @State private var presented: Bool = false
      
          var body: some View {
              NavigationSplitView {
                  List(PartyTask.allCases, selection: $selectedTask) { task in
                      NavigationLink(value: task) {
                          TaskLabel(task: task)
                      }
                      .listItemTint(task.color)
                  }
              } detail: {
                  if case .food = selectedTask {
                      FoodsListView()
                  } else {
                      selectedTask.flatMap { $0.color } ?? .white
                  }
              }
              .sheet(isPresented: $presented) {
                  Text("Budget View")
                      .presentationDetents([.height(250), .medium])
                      .presentationDragIndicator(.visible)
              }
          }
      }
      
    • 올해부터 타겟 하나로 멀티플랫폼 지원이 가능해졌음

    • 메뉴바의 추가 화면도 swiftUI로 지원 가능(macOS Ventura부터)

      • 아예 메뉴바만 있는 앱도 만들 수 있다.
      @main
      struct PartyPlanner: App {
          var body: some Scene {
              Window("Party Budget", id: "budget") {
                  Text("Budget View")
              }
      
              MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
                  BulletinBoard()
              }
              .menuBarExtraStyle(.window)
          }
      }
      
      @main
      struct MessageBoard: App {
          var body: some Scene {
              MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {
                  BulletinBoard()
              }
              .menuBarExtraStyle(.window)
          }
      }
      
  • Advanced controls → macos Ventura의 세팅앱이 이를 기반으로 완전히 변경됨

    • Forms - 스타일 변경

      
              Form {
                  Section {
                      LabeledContent("Location", value: address)
                      DatePicker("Date", selection: $date)
                      TextField("Description", text: $eventDescription, axis: .vertical)
                          .lineLimit(3, reservesSpace: true)
                  }
      
                  Section("Vibe") {
                      Picker("Accent color", selection: $accent) {
                          ForEach(Theme.allCases) { theme in
                              Text(theme.rawValue.capitalized).tag(theme)
                          }
                      }
                      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)
          }
      
    • Controls

      • 멀티라인 텍스트 필드 - 높이는 라인수에 따라 자동으로 조정됨

        Textfield("Description", text: $description, axis: .vertical)
        	.lineLimit(5...10) // 5줄 만큼은 미리 확보, 10줄 이상은 스크롤됨
        
      • 다중 dataPicker

        @State private var activityDates: Set<DateComponents> = [
                DateComponents(calendar: .current, year: 2022, month: 6, day: 6),
                DateComponents(calendar: .current, year: 2022, month: 6, day: 9),
                DateComponents(calendar: .current, year: 2022, month: 6, day: 10)
            ]
        
        Section("Dates") {
            MultiDatePicker("Activities Dates", selection: $activityDates)
        }
        
      • 여러 상태가 섞인 토글과 피커

      • 이제는 버튼뿐 아니라 버튼 같이 생긴 control들에도 buttomStyle 적용가능(toggle, menu, picker)

      • stepper에 포맷 인자 추가 및 watchOS 지원

        Stepper("Guest limit", value: $guestLimit, format: .number)
        
      • Accessibility Quick Action을 일반 뷰와 동일하게 적용 가능

        var body: some View {
                VStack(alignment: .leading) {
                    ItemDescriptionView()
                    addToCartButton
                }
                .accessibilityQuickAction(style: .prompt) {
                    addToCartButton
                }
            }
        
            var addToCartButton: some View {
                Button(isInCart ? "Remove from cart" : "Add to cart") {
                    isInCart.toggle()
                }
            }
        }
        
    • Tables

      • iPadOS에서 테이블 지원.(macOS에서는 작년에 제공되기 시작한 것)

        • 작은 사이즈(iPhone 등)일때는 첫번째 컬럼만 그려진다.
        @State private var attendees: [Attendee]
        
        var body: some View {
        	Table(attendees) {
        		TableColumn("Name") { attendee in
        			AttendeeRow(attendee)
        		}
        		
        		TableColumn("City", value: \\.city)
        		TableColumn("Status) { attendee in 
        			StatusRow(attendee)
        		}
        	}
        }
        
      • macOS에서는 테이블의 컨텍스트 메뉴도 쉽게 지정 가능

        #if os(macOS)
        .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) }
            }
        }
        #endif
        
      • 툴바 커스텀 기능(iPadOS, macOS)

        • id 기반으로 커스텀 툴바 설정을 swiftUI가 기억한다.
        • secondaryAction으로 지정해야만 커스텀이 가능하다.
        .toolbar(id: "toolbar") {
            ToolbarItem(id: "new", placement: .secondaryAction) {
                Button(action: {}) {
                    Label("New Invitation", systemImage: "envelope")
                }
            }
            ToolbarItem(id: "edit", placement: .secondaryAction) {
                Button(action: {}) {
                    Label("Edit", systemImage: "pencil.circle")
                }
            }
            ToolbarItem(id: "share", placement: .secondaryAction) {
                Button(action: {}) {
                    Label("Share", systemImage: "square.and.arrow.up")
                }
            }
            ToolbarItem(id: "tag", placement: .secondaryAction) {
                Button(action: {}) {
                    Label("Tags", systemImage: "tag")
                }
            }
            ToolbarItem(
                id: "reminder", placement: .secondaryAction, showsByDefault: false
            ) {
                Button(action: {}) {
                    Label("Set reminder", systemImage: "bell")
                }
            }
        }
        .toolbarRole(.editor)
        
    • 검색창 지원 추가

      • 토큰 검색 추가

      • 스코프 기능 추가 - 검색창 아래에 뜬다.

        @State private var queryText: String
        @State private var queryTokens: [InvitationToken]
        @State private var scope: AttendanceScope
        
        var body: some View {
        	InvitationsContentView()
        		.searchbars(text: $queryText, tokens: $queryTokens, scope: $scope) { token in
        				Label(token.displayName, systemImage: token.systemImage)
        		} scopes: {
        			Text("In Person").tag(AttendanceScope.inPerson)
        			Text("Online").tag(AttendanceScope.online)
        		}
        }
        
  • Sharing

    • Photos → 단일 선택용과 다중 선택용 모두 제공

      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")
                      }
                  }
          }
      }
      
    • Sharing

      .toolbar {
          PhotosPicker(
              selection: $viewModel.imageSelection,
              matching: .images
          ) {
              Label("Pick a photo", systemImage: "plus.app")
          }
          Button {
              viewModel.applyFilter()
          } label: {
              Label("Apply Filter", systemImage: "camera.filters")
          }
          if let item = viewModel.processedImage {
              ShareLink(
                  item: item, preview: SharePreview("Birthday Effects"))
          }
      }
      
    • Transferable: Photos와 Sharing의 기반

      • 스위프트로 만들어진, 특정 타입이 어떻게 앱 간에 전송될 수 있는지는 나타내는 방법

      • 드래그 앤 드롭 등에 활용됨

        // 어떤 타입의 데이터를 드래그 앤 드롭 했을 때 반응할지를 명시하면
        // 드롭된 데이터와, 그 위치값을 콜백에서 받음
        .dropDestination(payloadType: Image.self) { receivedImages, location in
            guard let image = receivedImages.first else {
                return false
            }
            viewModel.imageState = .success(image)
            return true
        }
        
      • 표준 Transferable 타입

        • String
        • Data
        • URL
        • AttributedString
        • Image
        • 등등…
      • 커스텀 타입도 가능 → 필요한 조건 등의 세부 사항은 다른 세션에서

  • Graphics and layout

    • ShapeStyle
      • gradient 넣기, 그림자 넣기 효과 추가 →

        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)
            }
        }
        
      • 텍스트 및 이미지 트랜지션

    • Layout