SwiftUIの新機能 〜その1〜 – WWDC2020

Session概要

SwiftUIを利用することで、iPhone、iPad、Mac、Apple Watch、Apple TV向けに、より良く、よりパワフルなアプリが構築できます。アウトライン、グリッド、ツールバーなどのインターフェイスの改善を含め、SwiftUIの最新機能について学びましょう。Appleでサインインなどの機能を実現するAppleフレームワーク全体に対し、強化されたSwiftUIだけでアプリを作成する方法、コンプリケーションや新しいウィジェットをカスタマイズする方法について

https://developer.apple.com/videos/play/wwdc2020/10041/

アプリとWidgets API

App protocolとScene

  • SwiftUIだけでアプリをビルドできるようになった
  • 新しい App protocolはViewコードですでに使用した宣言やStateドリブンパターンをフォローする
  • SwiftUIではmacOS向けに新規ウィンドウメニューコマンドのメインメニューへの自動的な追加も可能
@main
struct BookClubApp: App {
    @StateObject private var store = ReadingListStore()

    // App protocolではSceneを返す
    @SceneBuilder var body: some Scene {
        // SwiftUI内のSceneがボックスの外で、マルチプラットフォームに対応していることがわかる
        // iOS, watchOSでは単一の全画面ウィンドウを作成し、管理する
        WindowGroup {
            ReadingListViewer(store: store)
        }
        
    // macOS向けにアプリのメニューに設定を追加し、標準コマンドを自動的に設定する
    #if os(macOS)
        Settings {
            BookClubSettingsView()
        }
    #endif
    }
}

struct BookClubSettingsView: View {
    var body: some View {
        Text("Add your settings UI here.")
            .padding()
    }
}

struct ReadingListViewer: View {
    @ObservedObject var store: ReadingListStore

    var body: some View {
        NavigationView {
            List(store.books) { book in
                Text(book.title)
            }
            .navigationTitle("Currently Reading")
        }
    }
}

class ReadingListStore: ObservableObject {
    init() {}

    var books = [
        Book(title: "Book #1", author: "Author #1"),
        Book(title: "Book #2", author: "Author #2"),
        Book(title: "Book #3", author: "Author #3")
    ]
}

struct Book: Identifiable {
    let id = UUID()
    let title: String
    let author: String
}

Document groups

  • DocumentGroupはドキュメントベースのSceneの編集や保存をじどうてきに管理する
    • iOS, iPadOS, macOSでサポートされている
  • DocumentGroupがiOSとiPadOSでドキュメントブラウザを自動で表示するのは、他のメインインターフェイスが提供されていない場合です
  • Macでは新規ドキュメントごとに異なるウィンドウが開かれ、一般的なアクションが選べるコマンドをメインメニューに自動的に追加する
    • コマンドの追加はコマンドモディファイアを実装することで可能
import SwiftUI
import UniformTypeIdentifiers

@main
struct ShapeEditApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: ShapeDocument()) { file in
            DocumentView(document: file.$document)
        }
    }
}

struct DocumentView: View {
    @Binding var document: ShapeDocument
    
    var body: some View {
        Text(document.title)
            .frame(width: 300, height: 200)
    }
}

struct ShapeDocument: Codable {
    var title: String = "Untitled"
}

extension UTType {
    static let shapeEditDocument =
        UTType(exportedAs: "com.example.ShapeEdit.shapes")
}

extension ShapeDocument: FileDocument {
    static var readableContentTypes: [UTType] { [.shapeEditDocument] }
    
    init(fileWrapper: FileWrapper, contentType: UTType) throws {
        let data = fileWrapper.regularFileContents!
        self = try JSONDecoder().decode(Self.self, from: data)
    }

    func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws {
        let data = try JSONEncoder().encode(self)
        fileWrapper = FileWrapper(regularFileWithContents: data)
    }
}

コマンドモディファイア(カスタムコマンドの追加)

import SwiftUI
import UniformTypeIdentifiers

@main
struct ShapeEditApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: ShapeDocument()) { file in
            DocumentView(document: file.$document)
        }
        .commands {
            CommandMenu("Shapes") {
                Button("Add Shape...", action: addShape)
                    .keyboardShortcut("N")
                Button("Add Text", action: addText)
                    .keyboardShortcut("T")
            }
        }
    }
    
    func addShape() {}
    func addText() {}
}

struct DocumentView: View {
    @Binding var document: ShapeDocument
    
    var body: some View {
        Text(document.title)
            .frame(width: 300, height: 200)
    }
}

struct ShapeDocument: Codable {
    var title: String = "Untitled"
}

extension UTType {
    static let shapeEditDocument =
        UTType(exportedAs: "com.example.ShapeEdit.shapes")
}

extension ShapeDocument: FileDocument {
    static var readableContentTypes: [UTType] { [.shapeEditDocument] }
    
    init(fileWrapper: FileWrapper, contentType: UTType) throws {
        let data = fileWrapper.regularFileContents!
        self = try JSONDecoder().decode(Self.self, from: data)
    }

    func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws {
        let data = try JSONEncoder().encode(self)
        fileWrapper = FileWrapper(regularFileWithContents: data)
    }
}

Launch ScreenのInfo.plistキー

  • Launch Screenのコンポーネントを様々な組み合わせで宣言可能なキー
    • 例えば、デフォルトの画像や背景の色、トップバーやボトムバーを空にするかなどの宣言
  • Launch Screenにstoryboardを使用している場合は、引き続き使用可能

Widgets

  • WidgetsはSwiftUIでのみビルドされ、Viewと同じく Widgetプロトコルに対してカスタム構造体を使用する
  • ウィジェットの詳細な実装方法についてはこちらを参照
import SwiftUI
import WidgetKit

@main
struct RecommendedAlbum: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: "RecommendedAlbum",
            provider: Provider(),
            placeholder: PlaceholderView()
        ) { entry in
            AlbumWidgetView(album: entry.album)
        }
        .configurationDisplayName("Recommended Album")
        .description("Your recommendation for the day.")
    }
}

struct AlbumWidgetView: View {
    var album: Album

    var body: some View {
        Text(album.title)
    }
}

struct PlaceholderView: View {
    var body: some View {
        Text("Placeholder View")
    }
}

struct Album {
    var title: String
}

struct Provider: TimelineProvider {
    struct Entry: TimelineEntry {
        var album: Album
        var date: Date
    }

    public func snapshot(with context: Context, completion: @escaping (Entry) -> ()) {
        let entry = Entry(album: Album(title: "Untitled"), date: Date())
        completion(entry)
    }

    public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [Entry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = Entry(album: Album(title: "Untitled #\(hourOffset)"), date: entryDate)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

Apple Watchのカスタムコンプリケーション

ListとCollectionの表示における改善

アウトラインの構築

  • Listに子のキーパスを設定することでコンテンツの再帰的なアウトラインの構築が可能となった
struct OutlineContentView: View {
    var graphics: [Graphic]
    
    var body: some View {
        List(graphics, children: \.children) { graphic in
            GraphicRow(graphic)
        }
        .listStyle(SidebarListStyle())
    }
}

struct Graphic: Identifiable {
    var id: String
    var name: String
    var icon: Image
    var children: [Graphic]?
}

struct GraphicRow: View {
    var graphic: Graphic
    
    init(_ graphic: Graphic) {
        self.graphic = graphic
    }
    
    var body: some View {
        Label {
            Text(graphic.name)
        } icon: {
            graphic.icon
        }
    }
}

Grid

  • グリッドレイアウトの遅延読み込みをサポート
    • ScrollViewでサポートされ、コンテンツがなめらかにスクロールする
  • SwiftUIはグリッドの水平スクロールもサポート
  • StackView内の異なるレイアウトの画像切り替え機能として、ViewBuilderをサポート
  • Stack、Grid、アウトラインの詳細についてはこちらを参照
struct WildlifeList: View {
    var rows: [ImageRow]

    var body: some View {
        ScrollView {
            // 遅延読み込みのStack
            LazyVStack(spacing: 2) {
                ForEach(rows) { row in
                    // 異なるレイアウトの画像を表示する
                    switch row.content {
                    case let .singleImage(image):
                        SingleImageLayout(image: image)
                    case let .imageGroup(images):
                        ImageGroupLayout(images: images)
                    case let .imageRow(images):
                        ImageRowLayout(images: images)
                    }
                }
            }
        }
    }
}
最新情報をチェックしよう!