168
122

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

WWDC22、iOS16:数行のコードで作成できるSwiftUIの新機能(26本)

Last updated at Posted at 2022-06-14

※一般公開されているWWDC Keynoteの動画と公開Session/Documentation/Sample Codeページだけを使ってこの記事を執筆しました。

新しいビューの種類:
Gauge - 進捗状況を表示する新しいビュー
複数日付選択ツール
図表
複数列のテーブル
グリッド(格子)表示
貼り付けボタン
MacOS メニューバーアプリ
ナビゲーション・スタック
ナビゲーションのスプリットビュー

UIを変更する新しいビューモディファイア:
グラデーションカラーを生成する
インナーシャドウ、アウターシャドウの追加

SwiftUIで直接利用(互換性のあるビューは不要になりました)。:
共有シート
写真選択ツール
アプリのレビューを依頼

機能の改善:
サイズ変更可能なシート
SF Symbolカラーバリエーション
複数行のテキストフィールド
テキストフィールドを添付したアラート
スコープ選択で検索

新機能:
ビューのスクリーンショット画像を取得する
タップした位置の座標を取得
ビューが表示されたときにテキストフィールドにフォーカスする
スクロール時にキーボードが自動的に解除される
リストに対する削除・移動アクションの自動生成
MacOSで新しいウィンドウを追加して開く

その他の便利なコードスニペット:
iOSのバージョンに応じた条件付きビューモディファイア

お知らせ:beta1ではほとんどの機能が動作しますが、フォトピッカー、デフォルトフォーカスに一部不具合があります。

新しいビューの種類

Gauge - 進捗状況を表示する新しいビュー

円形のゲージビュー (accessoryCircular) を使用して進捗状況を表示することができます。

特定の値(value)や範囲(in)を定義することです。また、.tintビューモディファイアを使用すると、美しいグラデーションの色合いを追加することができます。

gauge_with_color.png

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)

ゲージのスタイルを指定しない場合、デフォルトで、通常のプログレスバーに似た線形ゲージが使用されます。

default_gauge.jpg

Gauge(value: displayedProgress) {
    Text("Default Gauge")
}

また、最小値位置/最大値位置/現在値位置のラベルを追加することも可能です。

default_gauge_withvalues.jpg

Gauge(value: 50, in: 0...100) {
    Text("Default Gauge")
} currentValueLabel: {
    Text("50%")
} minimumValueLabel: {
    Text("0%")
} maximumValueLabel: {
    Text("100%")
}

複数日付選択ツール

複数の日付を選択することができます。
タップすることで日付の選択・解除ができます。

multi_date_picker.jpg

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では表はデフォルトでリストになるそうです)。

20220608223332.jpg

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)は、その名の通り、定義されたグリッドにアイテムを表示します。

grid_demo.jpg

グリッド内の各ビュー要素では、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)
            }
        }
    }
}

貼り付けボタン

ユーザーのペーストボードにアクセスし、読み込むためのボタンです。
一般的には、データやオブジェクトのためのものです。
例えば、ユーザーがコピーした画像を読み込むためのボタンが考えられます。

paste_button_demo.jpg

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のメニューバーアプリを作ることができるようになりました。

20220608221528.jpg

@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内で LSUIElementtrue に設定します。

また、MenuBarExtraWindowGroup に追加することで、アプリに通常のアプリウィンドウとメニューバーアイテムの両方を表示させることができます。

ナビゲーション・スタック

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ブロックを使って詳細表示(ユーザーが左側の項目を選択すると右側に表示される)を行うことになります。

navigation_split.jpg

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 のような場所で適用することができます。

gradient_demo.jpg

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)
        }
    }
}

インナーシャドウ、アウターシャドウの追加

shadow_demo.jpg

影(シャドウ)は、シェイプの内側、または外側に付けることができます。

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ビューコンポーネントを使うことで、共有シートを提示することができます。

SWIFTUI-SHAREPLAY.jpg

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ではシミュレータで実行した場合、いくつかの問題があるようです。フォトピッカーは表示されますが、写真を選択しても何も起こりません)。

photo_picker.jpg

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#>)) はウィンドウシーンの入力が必要なので、これは便利です。

機能の改善

サイズ変更可能なシート

これで、様々なデテント(中、大、カスタムサイズ)でシートを表示し、ユーザーがドラッグしてシート全体を表示させることができます。

resizable_sheet.jpg

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カラーバリエーション

例えば、信号の強さを表すのに使用することができます。

varied_sfsymbol.jpg

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:multiline.jpg

TextField("Enter some text here", text: $userEnteredText, axis: .vertical)

また、lineLimitビューモディファイアを使用すると、行数を制限することができます。

テキストフィールドを添付したアラート

アラート内にテキストフィールドを追加できるようになりました。
ユーザーが入力した内容は、@Bindingを使用して変数に更新されます。

card-reading-demo-img-1.jpg

.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に移動アクションと削除ボタンを自動的に生成するように依頼することができます。これにより配列が直接更新されます。

automatic_list:actions.jpg

変数が変更されたときに、変更されたデータをデータベースに更新するために .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アプリに新しいウィンドウを追加することができます。また、ウィンドウを表示するためのキーボードショートカットを定義することができます。

window_demo.jpg

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()
}

:thumbsup: お読みいただきありがとうございました。

☺️ Twitter @MszPro
🐘 Mastodon @me@mszpro.com

:sunny:


writing-quickly_emoji_400.png

関連記事

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フレームワークを使ってチャートを作成する

WWDC22, iOS 16: WeatherKitで気象データを取得

WWDC 2022の基調講演のまとめ記事

168
122
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
168
122

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?