※一般公開されているWWDC Keynoteの動画と公開Session/Documentation/Sample Codeページだけを使ってこの記事を執筆しました。
新しいビューの種類:
Gauge - 進捗状況を表示する新しいビュー
複数日付選択ツール
図表
複数列のテーブル
グリッド(格子)表示
貼り付けボタン
MacOS メニューバーアプリ
ナビゲーション・スタック
ナビゲーションのスプリットビュー
UIを変更する新しいビューモディファイア:
グラデーションカラーを生成する
インナーシャドウ、アウターシャドウの追加
SwiftUIで直接利用(互換性のあるビューは不要になりました)。:
共有シート
写真選択ツール
アプリのレビューを依頼
機能の改善:
サイズ変更可能なシート
SF Symbolカラーバリエーション
複数行のテキストフィールド
テキストフィールドを添付したアラート
スコープ選択で検索
新機能:
ビューのスクリーンショット画像を取得する
タップした位置の座標を取得
ビューが表示されたときにテキストフィールドにフォーカスする
スクロール時にキーボードが自動的に解除される
リストに対する削除・移動アクションの自動生成
MacOSで新しいウィンドウを追加して開く
その他の便利なコードスニペット:
iOSのバージョンに応じた条件付きビューモディファイア
お知らせ:beta1ではほとんどの機能が動作しますが、フォトピッカー、デフォルトフォーカスに一部不具合があります。
新しいビューの種類
Gauge - 進捗状況を表示する新しいビュー
円形のゲージビュー (accessoryCircular) を使用して進捗状況を表示することができます。
特定の値(value)や範囲(in)を定義することです。また、.tint
ビューモディファイアを使用すると、美しいグラデーションの色合いを追加することができます。
Gauge(value: 23, in: 17...29) {
Text("°C")
} currentValueLabel: {
Text("23")
}
.gaugeStyle(.accessoryCircular)
.tint(Gradient(colors: [.green, .yellow, .orange]))
上記では、静的な値を使用しました。しかし、変数(@State
など)を使用することができます。
パーセント値(0から1まで)を使うこともできます。
Gauge(value: displayedProgress) {
Text("%")
} currentValueLabel: {
Text("\(Int(displayedProgress * 100))")
.font(.caption)
}
.gaugeStyle(.accessoryCircular)
ゲージのスタイルを指定しない場合、デフォルトで、通常のプログレスバーに似た線形ゲージが使用されます。
Gauge(value: displayedProgress) {
Text("Default Gauge")
}
また、最小値位置/最大値位置/現在値位置のラベルを追加することも可能です。
Gauge(value: 50, in: 0...100) {
Text("Default Gauge")
} currentValueLabel: {
Text("50%")
} minimumValueLabel: {
Text("0%")
} maximumValueLabel: {
Text("100%")
}
複数日付選択ツール
複数の日付を選択することができます。
タップすることで日付の選択・解除ができます。
struct MultipleDatePicker: View {
@State private var userSelectedDates: Set<DateComponents> = []
var body: some View {
Form {
MultiDatePicker("Select some dates", selection: $userSelectedDates)
Section {
Text("Selected dates:\(userSelectedDates.description)")
}
}
}
}
チャート
チャート(棒グラフ、折れ線グラフなど)を表示するには、Chartフレームワークを使用します。
以下の記事で紹介しています。
複数列のテーブル
複数の列がある表を表示することができます。
これはiPadでのみ利用可能です。
iPhoneのポートレートモードで表示した場合、1列目のみが表示されます(Appleのエンジニアによると、iPhoneでは表はデフォルトでリストになるそうです)。
struct Table_MultiColumns: View {
static let demoData: [MyPokemonFriend] = [
.init(name: "Pika", species: "Pikachu", status: "Sleeping"),
.init(name: "Zera", species: "Zeraora", status: "Charing my phone"),
.init(name: "Luca", species: "Lucario", status: "Hiking")
]
var body: some View {
Table(Table_MultiColumns.demoData) {
TableColumn("Name", value: \.name)
TableColumn("Species", value: \.species)
TableColumn("Status", value: \.status)
}
}
}
現在、beta1では、これに何らかの問題があるようです。タイトルがコンテンツと重なってしまう。
グリッド(格子)で表示
グリッド(Grid)は、その名の通り、定義されたグリッドにアイテムを表示します。
グリッド内の各ビュー要素では、gridCellColumns
ビュー修飾子を使用して、1つのビュー要素が占めるスペースを定義することができます。例えば、1つのビュー要素が2つのビュー要素のスペースを占有したい場合、 .gridCellColumns(2)
と定義します。
struct StaticGrid: View {
var body: some View {
Grid(horizontalSpacing: 20, verticalSpacing: 20) {
GridRow {
RoundedRectangle(cornerSize: .init(width: 20, height: 20))
.frame(width: 150, height: 150)
.foregroundColor(.blue)
.gridCellColumns(2) // this means this view element will be treated as 2 cell columns.
}
GridRow {
RoundedRectangle(cornerSize: .init(width: 20, height: 20))
.frame(width: 150, height: 150)
.foregroundColor(.blue)
RoundedRectangle(cornerSize: .init(width: 20, height: 20))
.frame(width: 150, height: 150)
.foregroundColor(.blue)
}
}
}
}
貼り付けボタン
ユーザーのペーストボードにアクセスし、読み込むためのボタンです。
一般的には、データやオブジェクトのためのものです。
例えば、ユーザーがコピーした画像を読み込むためのボタンが考えられます。
struct PasteButtonDemo: View {
@State private var userPastedImageObj: UIImage?
var body: some View {
Form {
PasteButton(supportedContentTypes: [.image]) { providers in
if let firstProvider = providers.first {
_ = firstProvider.loadDataRepresentation(for: .image) { data, error in
if let data,
let imageObj = UIImage(data: data) {
self.userPastedImageObj = imageObj
}
}
}
}
.buttonBorderShape(.capsule)
if let userPastedImageObj {
Image(uiImage: userPastedImageObj)
.resizable()
.scaledToFit()
}
}
}
}
MacOS メニューバーアプリ
SwiftUIアプリの中でMenuBarExtra
を使用することで、簡単にMacOSのメニューバーアプリを作ることができるようになりました。
@main
struct TestMenuBarAppApp: App {
var body: some Scene {
MenuBarExtra {
ContentView()
} label: {
Label("TODO", systemImage: "calendar.badge.clock")
}
.menuBarExtraStyle(.window)
}
}
メニューバーアプリがウィンドウを表示している場合は、ビューモディファイア . menuBarExtraStyle
を .window
に設定することができます。また、メニューバーアプリがメニューを表示している場合は、.menu
に設定することができます。
SwiftUI アプリ内で MenuBarExtra
のみを使用する場合、メニューバーアイテムのみが表示されます。Dockにあるアプリのアイコンを隠したい場合は、Info.plist
内で LSUIElement
を true
に設定します。
また、MenuBarExtra
を WindowGroup
に追加することで、アプリに通常のアプリウィンドウとメニューバーアイテムの両方を表示させることができます。
ナビゲーション・スタック
struct NavigationStackDemo: View {
var body: some View {
NavigationStack {
List {
ForEach(pokemonObjs) { pokemon in
NavigationLink(pokemon.name, value: pokemon)
}
}
.navigationDestination(for: Pokemon.self) { pokemon in
PokemonDetailView(pokemonObj: pokemon)
}
}
}
}
ここでは、NavigationStack
を使用します。
各 NavigationLink
には、ナビゲーションボタンを作成します。
navigationDestination
モディファイアでは、各リストアイテムのビューを指定します。
ナビゲーションのスプリットビュー
ナビゲーションの分割表示では、detail
ブロックを使って詳細表示(ユーザーが左側の項目を選択すると右側に表示される)を行うことになります。
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: {
selectedTask.flatMap { $0.color } ?? .white
}
}
}
UIを変更する新しいビューモディファイア
グラデーションカラーを生成する
SwiftUIの色に基づいてグラデーションを生成することができます。グラデーションは .fill
のような場所で適用することができます。
struct ColorGredientDemo: View {
var body: some View {
HStack {
Rectangle()
.fill(.blue.gradient)
.frame(width:120, height:120)
Rectangle()
.fill(.orange.gradient)
.frame(width:120, height:120)
Rectangle()
.fill(.green.gradient)
.frame(width:120, height:120)
}
}
}
インナーシャドウ、アウターシャドウの追加
影(シャドウ)は、シェイプの内側、または外側に付けることができます。
struct ShadowDemo: View {
var body: some View {
HStack {
Rectangle()
.fill(.blue.gradient.shadow(.inner(radius: 20)))
.frame(width:120, height:120)
Rectangle()
.fill(.blue.gradient.shadow(.drop(radius: 20)))
.frame(width:120, height:120)
}
}
}
SwiftUIで直接利用(互換性のあるビューは不要になりました)
共有シート
以前は、SwiftUIでシステムの共有シートでアイテムを共有するには、UIActivityViewControllerを使用する必要がありました。今では、ShareLink
ビューコンポーネントを使うことで、共有シートを提示することができます。
struct ShareLink_sharing_sheet: View {
var body: some View {
Form {
ShareLink("Share our URL", item: "View all SwiftUI components at https://ui.mszpro.com", preview: SharePreview("SwiftUI view components"))
}
}
}
文字列、画像、データ、URLなどのデータ型の共有ができるはずです。
写真選択ツール
(この機能は、ベータ1ではシミュレータで実行した場合、いくつかの問題があるようです。フォトピッカーは表示されますが、写真を選択しても何も起こりません)。
struct PhotosPicker_Demo: View {
@State private var selectedImage: PhotosPickerItem?
@State private var newlyPickedImage: UIImage?
var body: some View {
Form {
PhotosPicker("Pick a new background image", selection: $selectedImage, matching: .images)
.onChange(of: selectedImage) { newValue in
if let newValue {
newValue.loadTransferable(type: Data.self) { result in
switch result {
case .success(let success):
if let success,
let imageObj = UIImage(data: success)
{
self.newlyPickedImage = imageObj
}
case .failure(let failure):
return
}
}
}
}
if let newlyPickedImage {
Image(uiImage: newlyPickedImage)
.resizable()
.scaledToFit()
}
}
}
}
アプリのレビューを依頼
これで、環境変数を使ってSwiftUI内で簡単にアプリのレビューを要求できるようになりました。
import SwiftUI
import StoreKit
struct RequestAppReview: View {
@Environment(\.requestReview) var requestReview
var body: some View {
Button("Request review") {
requestReview()
}
}
}
SwiftUI では、アプリのレビューを要求する通常の関数 (SKStoreReviewController.requestReview(in: <#T##UIWindowScene#>)
) はウィンドウシーンの入力が必要なので、これは便利です。
機能の改善
サイズ変更可能なシート
これで、様々なデテント(中、大、カスタムサイズ)でシートを表示し、ユーザーがドラッグしてシート全体を表示させることができます。
struct Resizable_sheets: View {
@State private var showSheet: Bool = false
var body: some View {
Button("Present sheet") {
self.showSheet = true
}
.sheet(isPresented: $showSheet) {
Text("Hello world!")
.presentationDetents([.medium, .large])
}
}
}
SF Symbolカラーバリエーション
例えば、信号の強さを表すのに使用することができます。
struct SFSymbol_variedColor: View {
@State private var wifiSignalStrength: Double = 1.0
var body: some View {
Form {
Image(systemName: "wifi", variableValue: wifiSignalStrength)
Slider(value: $wifiSignalStrength, in: 0...1)
}
}
}
複数行のテキストフィールド
SwiftUIのテキストフィールドが複数行をサポートするようになりました。縦に展開されます。
TextField("Enter some text here", text: $userEnteredText, axis: .vertical)
また、lineLimit
ビューモディファイアを使用すると、行数を制限することができます。
テキストフィールドを添付したアラート
アラート内にテキストフィールドを追加できるようになりました。
ユーザーが入力した内容は、@Binding
を使用して変数に更新されます。
.alert
ビューモディファイア宣言の後に要素のブロックを追加することになります。
struct Alert_with_TextField: View {
@State private var typedPasscode: String = ""
@State private var showPasscodeAlert: Bool = false
var body: some View {
Form {
Button("Enter passcode and NFC scan") {
self.typedPasscode = ""
self.showPasscodeAlert = true
}
.alert("Enter card passcode", isPresented: $showPasscodeAlert) {
TextField("Passcode", text: $typedPasscode)
Button("Confirm") {
print(typedPasscode)
}
Button("Cancel", role: .cancel, action: {})
}
}
}
}
スコープ選択で検索
スコープを選択した検索ビューを提供することができます。
List(filteredResults) { result in
Text(result.name)
}
.searchable(text: $searchText, scope: $searchScope) {
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.rawValue)
}
新機能
ビューのスクリーンショット画像を取得する
SwiftUIのビューを画像にレンダリングすることができるようになりました。
いくつかのコントロールビュー(ステッパーのようなユーザーインタラクティブビュー)はレンダリングできないことに注意してください。レンダリングできないビューには、×印が表示されます。(beta1では)
struct Render_view_toImage: View {
@State private var currentProgressValue: Double = 0
@State private var renderedImage: UIImage?
var body: some View {
Form {
gauge_experimentation_view
Section {
Button("Save view to image") {
let renderer = ImageRenderer(content: gauge_experimentation_view)
if let image = renderer.uiImage {
self.renderedImage = image
}
}
}
Section {
if let renderedImage {
Image(uiImage: renderedImage)
}
}
}
}
var gauge_experimentation_view: some View {
Section {
Gauge(value: self.currentProgressValue, in: 0...100) {
Text("Default Gauge")
} currentValueLabel: {
Text("\(currentProgressValue)%")
} minimumValueLabel: {
Text("0%")
} maximumValueLabel: {
Text("100%")
}
Stepper("Gauge value", value: $currentProgressValue, in: 0...100, format: .number)
}
}
}
タップした位置の座標を取得
ユーザーのタップジェスチャーを検出する onTap
ジェスチャーを使用した場合、ユーザーがタップした場所を取得できるようになりました。
struct OnTap_Gesture_Location: View {
var body: some View {
Form {
Rectangle()
.fill(.blue)
.frame(width: 300, height: 500)
.onTapGesture(coordinateSpace: .local) { tappedLocation in
print(tappedLocation)
}
}
}
}
上の例ではローカル(local)座標空間を使いましたが、これは矩形に対する相対的な座標を意味します。画面に対する相対的な座標を得るためにグローバル(global)座標空間を使うこともできる。
ビューが表示されたときにテキストフィールドにフォーカスする
(この機能は、beta1のXcodeシミュレータでは動作しないようです。具体的には、ビューが表示されたときにテキストフィールドが選択されていないのです)。
ビューが表示されたときに特定のテキストフィールドにフォーカスを当てるようにSwiftUIに要求できるようになりました。まず、@FocusState
変数を追加する必要があります。それから、defaultFocus
ビューモディファイアを使います。
WindowGroup {
VStack {
TextField(...)
.focused($focusedField, equals: .firstField)
TextField(...)
.focused($focusedField, equals: .secondField)
}
.defaultFocus($focusedField, .secondField)
}
スクロール時にキーボードが自動的に解除される
struct ScrollDismissKeyboardDemo: View {
@State private var userEnteredText: String = ""
var body: some View {
Form {
TextField("Text entry", text: $userEnteredText)
.scrollDismissesKeyboard(.interactively)
}
.scrollIndicators(.hidden)
}
}
リストに対する削除・移動アクションの自動生成
さて、Listに配列を表示する場合、SwiftUIに移動アクションと削除ボタンを自動的に生成するように依頼することができます。これにより配列が直接更新されます。
変数が変更されたときに、変更されたデータをデータベースに更新するために .onChange
を使用する必要があるかもしれません。
struct AutomaticListEditActions: View {
@State var items = ["Item 1", "Item 2", "Item 3"]
var body: some View {
Form {
List($items, id: \.self, edits: [.delete, .move]) { $item in
Text(item)
}
Text(items.joined(separator: ","))
}
}
}
MacOSで新しいウィンドウを追加して開く
SwiftUIを使って、MacOSアプリに新しいウィンドウを追加することができます。また、ウィンドウを表示するためのキーボードショートカットを定義することができます。
SwiftUIのアプリファイルでWindow
ビューコンポーネントを定義することによって、それを行います。
@main
struct WindowsNewTestingApp: App {
var body: some Scene {
WindowGroup("Main View") {
ContentView()
}
Window("New window", id: "new_window") {
Text("New window here!")
}
.keyboardShortcut("0")
.defaultPosition(.topLeading)
.defaultSize(width: 220, height: 250)
}
}
上記の例では、キーボードショートカットを "0 "キーと定義しています。
これは、commandキーと0キーを意味します。
また、defaultPosition
ビューモディファイアを使ってデフォルトの位置を定義したり、defaultSize
ビューモディファイアを使ってデフォルトのサイズを定義したりすることができます。
アプリ内では、そのウィンドウを開くためのボタンも定義することができます。
struct DetailView: View {
@Environment(\.openWindow) var openWindow
var body: some View {
Text("Detail View")
.toolbar {
Button {
openWindow(id: "new_window")
} label: {
Image(systemName: "dollarsign")
}
}
}
}
Windowビューコンポーネントを作成する際に、そのidをnew_window
と定義しています。そのウィンドウをボタンで開きたい場合は、openWindow
関数に開きたいウィンドウのidを指定して使用します。
その他の便利なコードスニペット
iOSのバージョンに応じた条件付きビューモディファイア
iOS 16では、多くの新しいビューモディファイアが利用できるようになりました。システムのバージョンに応じて条件付きでモディファイアを使用したい場合。以下のコードを使用することができます。
let common = commonViewParts
if #available(iOS 16.0, macOS 13.0, *) {
return common.newModifier()
} else {
return common.oldModifier()
}
お読みいただきありがとうございました。
☺️ Twitter @MszPro
🐘 Mastodon @me@mszpro.com
関連記事
・UICollectionViewの行セル、ヘッダー、フッター、またはUITableView内でSwiftUIビューを使用(iOS 16, UIHostingConfiguration)
・iPhone 14 ProのDynamic Islandにウィジェットを追加し、Live Activitiesを開始する(iOS16.1以降)
・iOS 16:秘密値の保存、FaceID認証に基づく個人情報の表示/非表示(LARight)
・iOS16 MapKitの新機能 : 地図から場所を選ぶ、通りを見回す、検索補完
・SwiftUIアプリでバックグラウンドタスクの実行(ネットワーク、プッシュ通知) (BackgroundTasks, URLSession)
・WWDC22、iOS16:iOSアプリに画像からテキストを選択する機能を追加(VisionKit)
・WWDC22、iOS16:数行のコードで作成できるSwiftUIの新機能(26本)
・WWDC22、iOS 16:SwiftUIでChartsフレームワークを使ってチャートを作成する