目次
Session概要
SwiftUIの新機能 〜その1〜 では、SwiftUIのprotocol(App, Scene)についてやDocument Groupについて軽く触れ、コマンドモディファイアやWidget、及びGridの簡単なサンプルを紹介しました。ここでは、その他UI componentのためのAPIについて紹介した後、styleやアニメーションについて説明し、最後にシステムの統合方法について紹介します。
ツールバーやコントロールのためのAPI
ツールバーモディファイア
- ツールバーの表示をカスタマイズする
- ToolBarItemsはSwiftUIの他の部分と同じViewで構成されている
- セマンティックプレースメント
- ToolBarItemの役割をSwiftUIが理解し、自動的に適切な場所を見つける
- ツールバーに設定されるLabelはアクセシビリティの向上のため、タイトルがVoiceOverの対象として読まれる
struct ContentView: View {
var body: some View {
List {
Text("Book List")
}
.toolbar {
Button(action: recordProgress) {
Label("Record Progress", systemImage: "book.circle")
}
}
}
private func recordProgress() {}
}
// 確認とキャンセルのモーダルアクション サンプル
struct ContentView: View {
var body: some View {
Form {
Slider(value: .constant(0.39))
}
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Save", action: saveProgress)
}
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: dismissSheet)
}
}
}
private func saveProgress() {}
private func dismissSheet() {}
}
// アプリでアイテムを目立たせるためのプリンシパル配置のサンプル
struct ContentView: View {
enum ViewMode {
case details
case notes
}
@State private var viewMode: ViewMode = .details
var body: some View {
List {
Text("Book Detail")
}
.toolbar {
ToolbarItem(placement: .principal) {
Picker("View", selection: $viewMode) {
Text("Details").tag(ViewMode.details)
Text("Notes").tag(ViewMode.notes)
}
}
}
}
}

キーボードショートカットモディファイア
- iPadやMac向けにキーボードショートカットを追加する
@main
struct BookClubApp: App {
var body: some Scene {
WindowGroup {
List {
Text("Reading List Viewer")
}
}
.commands {
Button("Previous Book", action: selectPrevious)
.keyboardShortcut("[")
Button("Next Book", action: selectNext)
.keyboardShortcut("]")
}
}
private func selectPreviousBook() {}
private func selectNextBook() {}
}
プログレスビュー
- 時間経過と共に確定と不確定の進行状況を表示するのに使用する
- 線形と円形のプログレスビューがある
// 線形プログレス
struct ContentView: View {
var percentComplete: Double
var body: some View {
ProgressView("Downloading Photo", value: percentComplete)
}
}
// 円形プログレス
struct ContentView: View {
var percentComplete: Double
var body: some View {
ProgressView("Downloading Photo", value: percentComplete)
.progressViewStyle(CircularProgressViewStyle())
}
}
ゲージ
- プログレスビューと似ているが、全体的な容量と比較し値のレベルを表すのに使用する
- Delegateによりカスタマイズ可能で、現在値の取得などが可能
struct ContentView: View {
var acidity: Double
var body: some View {
Gauge(value: acidity, in: 3...10) {
Label("Soil Acidity", systemImage: "drop.fill")
.foregroundColor(.green)
} currentValueLabel: {
Text("\(acidity, specifier: "%.1f")")
} minimumValueLabel: {
Text("3")
} maximumValueLabel: {
Text("10")
}
}
}

スタイリングと視覚効果
- macOS Big Surでメニューバーの通知センターとコントロールセンターが刷新された
- 両方ともSwiftUIで実装されており、スタイルやアニメーションが適用されている
matchedGeometryEffect
- GridやStackなどから流れるようなエフェクトを適用する
- SwiftUIが自動的にフレーム補間し、シームレスに遷移する
struct ContentView: View {
@Namespace private var namespace
@State private var selectedAlbumIDs: Set<Album.ID> = []
var body: some View {
VStack(spacing: 0) {
ScrollView {
albumGrid.padding(.horizontal)
}
Divider().zIndex(-1)
selectedAlbumRow
.frame(height: AlbumCell.albumSize)
.padding(.top, 8)
}
.buttonStyle(PlainButtonStyle())
}
private var albumGrid: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: AlbumCell.albumSize))], spacing: 8) {
ForEach(unselectedAlbums) { album in
Button(action: { select(album) }) {
AlbumCell(album)
}
.matchedGeometryEffect(id: album.id, in: namespace)
}
}
}
private var selectedAlbumRow: some View {
HStack {
ForEach(selectedAlbums) { album in
AlbumCell(album)
.matchedGeometryEffect(id: album.id, in: namespace)
}
}
}
private var unselectedAlbums: [Album] {
Album.allAlbums.filter { !selectedAlbumIDs.contains($0.id) }
}
private var selectedAlbums: [Album] {
Album.allAlbums.filter { selectedAlbumIDs.contains($0.id) }
}
private func select(_ album: Album) {
withAnimation(.spring(response: 0.5)) {
_ = selectedAlbumIDs.insert(album.id)
}
}
}
struct AlbumCell: View {
static let albumSize: CGFloat = 100
var album: Album
init(_ album: Album) {
self.album = album
}
var body: some View {
album.image
.frame(width: AlbumCell.albumSize, height: AlbumCell.albumSize)
.background(Color.pink)
.cornerRadius(6.0)
}
}
struct Album: Identifiable {
static let allAlbums: [Album] = [
.init(name: "Sample", image: Image(systemName: "music.note")),
.init(name: "Sample 2", image: Image(systemName: "music.note.list")),
.init(name: "Sample 3", image: Image(systemName: "music.quarternote.3")),
.init(name: "Sample 4", image: Image(systemName: "music.mic")),
.init(name: "Sample 5", image: Image(systemName: "music.note.house")),
.init(name: "Sample 6", image: Image(systemName: "tv.music.note"))
]
var name: String
var image: Image
var id: String { name }
}
Container Relative Shape
- 隣接するシェイプと類似した、パスをたどる新しい機能
- オフセットを考慮して自動的に同心性を保つ
struct AlbumWidgetView: View {
var album: Album
var body: some View {
album.image
.clipShape(ContainerRelativeShape())
.padding()
}
}
struct Album {
var name: String
var artist: String
var image: Image
}

Dynamic Type Scaling
- カスタムフォントはDynamic Typeの変更に自動で対応する
- アクセシビリティのサイズが大きくなっても、レスポンシブの高いカスタムレイアウトを作ることが可能
struct ContentView: View {
var album: Album
@ScaledMetric private var padding: CGFloat = 10
var body: some View {
VStack {
Text(album.name)
.font(.custom("AvenirNext-Bold", size: 30))
Text("\(Image(systemName: "music.mic")) \(album.artist)")
.font(.custom("AvenirNext-Bold", size: 17))
}
.padding(padding)
.background(RoundedRectangle(cornerRadius: 16, style: .continuous).fill(Color.purple))
}
}
struct Album {
var name: String
var artist: String
var image: Image
}

List item tinting
- リスト要素を個別に色をカスタマイズする
- ItemやSection全体に適用可能
struct ContentView: View {
var body: some View {
NavigationView {
List {
Label("Menu", systemImage: "list.bullet")
Label("Favorites", systemImage: "heart")
.listItemTint(.red)
Label("Rewards", systemImage: "seal")
.listItemTint(.purple)
Section(header: Text("Recipes")) {
ForEach(1..<4) {
Label("Recipes \($0)", systemImage: "book.closed")
}
}
.listItemTint(.monochrome)
}
.listStyle(SidebarListStyle())
}
}
}

SwiftUIアプリのシステムの統合
- URLを開く高機能APIが全てのプラットフォームで利用可能となった
- 例えば、Link ViewでURLを開きリンクをラベル付けする
- ユニバーサルリンクから他のアプリに遷移することも可能
- openURL オプションでcompletion handlerでURLを開くことが可能
- 特定のViewなので自動的にSwiftUIがウィンドウ上にURLを開く
- iPad OS 13の更新でSwiftUIはドラッグ&ドロップによる他アプリとのやり取りが可能となった
- Uniform Type Identifiersを使用することによりiPad OS 14からドラッグされるコンテンツの識別子が把握可能となった
- アプリ全体で適用され、カスタムエクスポート・インポートやイントロスペクション(オブジェクトの情報を参照する)に使用でき、記述を読みやすくしたり、適合性を検証することも可能
let appleURL = URL(string: "https://developer.apple.com/tutorials/swiftui/")!
let wwdcAnnouncementURL = URL(string: "https://apple.news/AjriX1CWUT-OfjXu_R4QsnA")!
struct ContentView: View {
var body: some View {
Form {
Section {
// リンクタップ後、Defaultのブラウザで開く
Link(destination: apple) {
Label("SwiftUI Tutorials", systemImage: "swift")
}
// リンクタップ後、ニュースアプリに遷移
Link(destination: wwdcAnnouncementURL) {
Label("WWDC 2020 Announcement", systemImage: "chevron.left.slash.chevron.right")
}
}
}
}
}
// openURLオプション
let customPublisher = NotificationCenter.default.publisher(for: .init("CustomURLRequestNotification"))
let apple = URL(string: "https://developer.apple.com/tutorials/swiftui/")!
struct ContentView: View {
@Environment(\.openURL) private var openURL
var body: some View {
Text("OpenURL Environment Action")
.onReceive(customPublisher) { output in
if output.userInfo!["shouldOpenURL"] as! Bool {
openURL(apple)
}
}
}
}
import UniformTypeIdentifiers
extension UTType {
static let myFileFormat = UTType(exportedAs: "com.example.myfileformat")
}
func introspecContentType(_ fileURL: URL) throws {
// Get this file's content type.
let resourceValues = try fileURL.resourceValues(forKeys: [.contentTypeKey])
if let type = resourceValues.contentType {
// Get the human presentable description of the type.
let description = type.localizedDescription
if type.conforms(to: .myFileFormat) {
// The file is our app’s format.
} else if type.conforms(to: .image) {
// The file is an image.
}
}
}s

Sign in with Apple
- AuthenticationServicesとSwiftUIをimportすることで新しいAPIが使用可能
- 全てのプラットフォームで使用可能
import AuthenticationServices
import SwiftUI
struct ContentView: View {
var body: some View {
SignInWithAppleButton(
.signUp,
onRequest: handleRequest,
onCompletion: handleCompletion
)
.signInWithAppleButtonStyle(.black)
}
private func handleRequest(request: ASAuthorizationAppleIDRequest) {}
private func handleCompletion(result: Result<ASAuthorization, Error>) {}
}