KeyPathを使って再帰的な構造体にアクセスしやすくする

1.1K Views

August 29, 25

スライド概要

profile-image

Swiftを書いています

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

KeyPathを使って再帰的な構造体にアクセスしやすくする 2025/07/10 pixiv App Night / ockey

2.

• 2025年4月新卒入社 • PalcyのiOSアプリエンジニア • 好きなSwift Ockey - enum - guard - KeyPath

3.

KeyPath、使っていますか?

4.

import SwiftUI struct NumbersList: View { let numbers = [0, 1, 2] var body: some View { List(numbers, id: \.self) { number in Text("\(number)") } } }

5.

import SwiftUI これ struct NumbersList: View { let numbers = [0, 1, 2] var body: some View { List(numbers, id: \.self) { number in Text("\(number)") } } }

6.

KeyPathを使うと、 どのプロパティにアクセスするかを 型安全に制御できる

7.

KeyPathのおかげで解決できた課題に ついてお話しします

8.

目次 1. KeyPathとは 2. 再帰的な構造体 3. 再帰的な構造体で直面した課題 4. KeyPathを使った方針 5. KeyPathを動的に生成する 6. まとめ

9.

KeyPathとは

10.

KeyPathとは https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/#Key-Path-Expression

11.

KeyPathとは 1. 簡単な使い方 2. 型が同じプロパティ 3. Array 4. Optional 5. Dictionary

12.
[beta]
KeyPathとは|簡単な使い方
struct Person {
var name: String
var emailAddress: String
var age: Int
}
let me = Person(
name: "ockey",
emailAddress: "[email protected]",
age: 10
)

2つの型を設定する
• Root: プロパティの持ち主
• Value: プロパティ

let toName: KeyPath<Person, String> = \Person.name

13.
[beta]
KeyPathとは|簡単な使い方
struct Person {
var name: String
var emailAddress: String
var age: Int
}
let me = Person(
name: "ockey",
emailAddress: "[email protected]",
age: 10
)
let toName: KeyPath<Person, String> = \.name

Rootは型推論が効くので省略可

14.
[beta]
KeyPathとは|簡単な使い方
struct Person {
var name: String
var emailAddress: String
var age: Int
}
let me = Person(
name: "ockey",
emailAddress: "[email protected]",
age: 10
)
let toName: KeyPath<Person, String> = \.name
me[keyPath: toName]
// "ockey"

[keyPath:]サブスクリプトに渡すと
プロパティにアクセスできる

15.
[beta]
KeyPathとは|型が同じプロパティ
struct Person {
var name: String
var emailAddress: String
var age: Int
}

型が同じであれば、別のプロパティにも
アクセスできる

let me = Person(
name: "ockey",
emailAddress: "[email protected]",
age: 10
)
var toString: KeyPath<Person, String> = \.name
me[keyPath: toString]
// "ockey"
toString = \.emailAddress
me[keyPath: toString]
// "[email protected]"

16.

KeyPathとは|Array(1/3) struct Person { var name: String var emailAddresses: [String] var age: Int } Arrayなプロパティも指定できる let me = Person( name: "ockey", emailAddresses: ["[email protected]", "[email protected]"], age: 10 ) me[keyPath: \.emailAddresses] // ["[email protected]", "[email protected]"]

17.

KeyPathとは|Array(2/3) struct Person { var name: String var emailAddresses: [String] var age: Int } Arrayの要素をインデックスで指定できる let me = Person( name: "ockey", emailAddresses: ["[email protected]", "[email protected]"], age: 10 ) me[keyPath: \.emailAddresses[0]] // "[email protected]"

18.

KeyPathとは|Array(3/3) struct Person { var name: String var emailAddresses: [String] var age: Int } 要素のプロパティも指定できる let me = Person( name: "ockey", emailAddresses: ["[email protected]", "[email protected]"], age: 10 ) me[keyPath: \.emailAddresses[0]].count // 14

19.
[beta]
KeyPathとは|Optional(1/3)
struct Person {
var name: String
var middleName: String?
var age: Int
}

Optional型のプロパティを指定できる

let me = Person(
name: "ockey",
age: 10
)
let toMiddleName: KeyPath<Person, String?> = \.middleName
me[keyPath: toMiddleName]
// nil

20.
[beta]
KeyPathとは|Optional(2/3)
struct Person {
var name: String
var middleName: String?
var age: Int
}

Optional Chainingできる
ValueはOptional型になる

let me = Person(
name: "ockey",
middleName: "Swift",
age: 10
)
let toMiddleNameCount: KeyPath<Person, Int?> = \.middleName?.count
me[keyPath: toMiddleNameCount]
// 5

21.
[beta]
KeyPathとは|Optional(3/3)
struct Person {
var name: String
var middleName: String?
var age: Int
}

RootをOptional型にすると、
ValueもOptional型になる

let who: Person? = Person(
name: "ockey",
age: 10
)
let toName: KeyPath<Person?, String?> = \.?.name
who[keyPath: toName]
// "ockey"

22.
[beta]
KeyPathとは|Dictionary(1/2)
struct Book: Identifiable {
let id: UUID
let name: String
}
struct Person {
var name: String
var books: [UUID: Book]
}

Keyを指定してDictionaryのValueに
アクセスできる
ValueはOptionalになる

let id = UUID()
let book = Book(id: id, name: "Swift Masters Guide")
let me = Person(name: "ockey", books: [id: book])
let toBook: KeyPath<Person, Book?> = \.books[id]
me[keyPath: toBook]
// Book

23.
[beta]
KeyPathとは|Dictionary(2/2)
struct Book: Identifiable {
let id: UUID
let name: String
}

Optional ChainingでValueの
プロパティにアクセスできる

struct Person {
var name: String
var books: [UUID: Book]
}
let id = UUID()
let book = Book(id: id, name: "Swift Masters Guide")
let me = Person(name: "ockey", books: [id: book])
let toBookName: KeyPath<Person, String?> = \.books[id]?.name
me[keyPath: toBookName]
// "Swift Masters Guide"

24.

再帰的な構造体

25.

再帰的な構造体 Swiftで書かれたコードを解析するツールを開発中 • 型とメンバの構造をツリー形式で表示する • シンボル同士の依存関係を可視化する

26.

再帰的な構造体 ツリー形式で表示するために、階層構造を維持したまま宣言を抽出したい @Observable class MainViewModel { var count: Int func increment() { count += 1 } } init(count: Int) { self.count = count } • MainViewModel - count - increment () - init()

27.

再帰的な構造体 ツリーのViewで扱いやすいように、構造体を使って階層構造を表したい Swiftでは[Self]型のプロパティを宣言できる Self型はコンパイルエラーになる struct Type { var children: [Self] } // Value type 'Type' cannot have a stored property that recursively contains it var child: Self

28.

再帰的な構造体 [Self]型のプロパティを持つ構造体を、この発表では再帰的な構造体と呼ぶ struct Type { var children: [Self] }

29.

再帰的な構造体 型やメンバの階層構造を維持したまま抽出するために、宣言1つを 表す再帰的なDeclaration構造体を宣言する struct Declaration: Identifiable { let id: String let name: String // 宣言に関するその他の情報 } var properties: [Self] var methods: [Self] var initializers: [Self]

30.

再帰的な構造体 Declaration構造体にメンバを格納していくことで、階層構造を維持できる @Observable class MainViewModel { var count: Int func increment() { count += 1 } } init(count: Int) { self.count = count } • MainViewModel • Declaration - count - Declaration - increment () - Declaration - init() - Declaration

31.

再帰的な構造体で直面した課題

32.

再帰的な構造体で直面した課題 コンパイラは各シンボルに一意な識別子(USR)を割り当てる Symbol Symbol Symbol USR USR USR

33.

再帰的な構造体で直面した課題 Declaration構造体のidは、コンパイラが各シンボルに割り当てた 一意な識別子(USR)を使う Symbol Symbol Symbol USR struct Declaration: Identifiable { let id: String let name: String // 宣言に関するその他の情報 USR USR } var properties: [Self] var methods: [Self] var initializers: [Self]

34.

再帰的な構造体で直面した課題 シンボルの依存関係は、参照する側とされる側のUSRの組として 取得できる Symbol Symbol Symbol USR USR USR

35.

再帰的な構造体で直面した課題 ツールでシンボルを選択すると、依存関係がある他のシンボルを リスト形式で表示したい Symbol Symbol Symbol USR USR USR Declaration Declaration Declaration

36.

再帰的な構造体で直面した課題 依存関係があるシンボルに対応するDeclarationインスタンスに、 USRを元にしてアクセスできるようにする必要がある Symbol Symbol Symbol USR USR USR Declaration Declaration Declaration

37.

再帰的な構造体で直面した課題 ネストが深いと、USRがわかっても簡単にはアクセスできない func functionA() { func functionB() {} func functionC() { func functionD() { func functionE() { func functionG() {} func functionH() {} func functionI() {} } func functionF() { func functionJ() {} } } } } • functionA - functionB - functionC • functionD - functionE • functionG • functionH • functionI - functionF • functionJ

38.

再帰的な構造体で直面した課題 USRが一致する要素を見つけるまで毎回探索していると効率が悪い • functionA - functionB - functionC • functionD - functionE • functionG • functionH • functionI - functionF • functionJ • functionA - functionB - functionC • functionD - functionE • functionG • functionH • functionI - functionF • functionJ

39.

再帰的な構造体で直面した課題 USRをKeyとするDictionaryを生成するとO(1)で見つけられる struct Declaration: Identifiable { let id: String let name: String // 宣言に関するその他の情報 var properties: [Self] = [] var methods: [Self] = [] var initializers: [Self] = [] } var table: [String: Declaration] = [:] let id = "sampleID" let function = Declaration(id: id, name: "sampleFunction") table[id] = function if let function = table[id] {}

40.

再帰的な構造体で直面した課題 孫、子、親をそれぞれValueに格納すると孫が3つ存在してしまう let grandchildID = "grandchildID" let grandchild = Declaration( id: grandchildID, name: "grandchildFunction" ) table[grandchildID] = grandchild let childID = "childID" let child = Declaration( id: childID, name: "childFunction", methods: [grandchild] ) table[childID] = child • grandchild • child - grandchild let parentID = "parentID" let parent = Declaration( id: parentID, name: "parentFunction", methods: [child] ) table[parentID] = parent • parent - child • grandchild

41.

再帰的な構造体で直面した課題 今後の拡張性を考慮するとSingle Source of Truthに反する実装は避けたい let grandchildID = "grandchildID" let grandchild = Declaration( id: grandchildID, name: "grandchildFunction" ) table[grandchildID] = grandchild let childID = "childID" let child = Declaration( id: childID, name: "childFunction", methods: [grandchild] ) table[childID] = child • grandchild • child - grandchild let parentID = "parentID" let parent = Declaration( id: parentID, name: "parentFunction", methods: [child] ) table[parentID] = parent • parent - child • grandchild

42.

Dictionaryで効率よくアクセスしたい かつ Single Source of Truthも維持したい

43.

DictionaryのValueに アクセスしたい要素そのものではなく 要素までアクセスする方法を格納したい

44.

KeyPathがある!

45.

KeyPathを使った方針

46.

KeyPathを使った方針 • 最も階層が浅いインスタンス1つをRootとする • RootをSingle Source of Truthとする • DictionaryのValueには、Rootからその要素までのKeyPathを格納する

47.

KeyPathを使った方針 struct Declaration: Identifiable { let id: String let name: String // その他のプロパティ 型やメンバの宣言1つに対応する Declaration構造体 } struct File: Identifiable { var id: String { fullPath } let fullPath: String var declarations: [Declaration] } struct Directory: Identifiable { var id: String { fullPath } let fullPath: String var subdirectories: [Self] var files: [File] } struct RootDirectory { var directory: Directory var keyPathTable: [String: KeyPath<Directory, Declaration>] }

48.

KeyPathを使った方針 struct Declaration: Identifiable { let id: String let name: String // その他のプロパティ Swiftファイル1つに対応するFile構造体 } struct File: Identifiable { var id: String { fullPath } let fullPath: String var declarations: [Declaration] } ファイル内の宣言を持つ struct Directory: Identifiable { var id: String { fullPath } let fullPath: String var subdirectories: [Self] var files: [File] } struct RootDirectory { var directory: Directory var keyPathTable: [String: KeyPath<Directory, Declaration>] }

49.

KeyPathを使った方針 struct Declaration: Identifiable { let id: String let name: String // その他のプロパティ ディレクトリ1つに対応するDirectory構造体 } struct File: Identifiable { var id: String { fullPath } let fullPath: String var declarations: [Declaration] } サブディレクトリとファイルを持つ struct Directory: Identifiable { var id: String { fullPath } let fullPath: String var subdirectories: [Self] var files: [File] } struct RootDirectory { var directory: Directory var keyPathTable: [String: KeyPath<Directory, Declaration>] }

50.

KeyPathを使った方針 struct Declaration: Identifiable { let id: String let name: String // その他のプロパティ } struct File: Identifiable { var id: String { fullPath } let fullPath: String var declarations: [Declaration] } struct Directory: Identifiable { var id: String { fullPath } let fullPath: String var subdirectories: [Self] var files: [File] } 最も階層が浅いディレクトリに対応する RootDirectory構造体 DirectoryからDeclarationまでのKeyPathを 格納するDictionaryを持つ struct RootDirectory { var directory: Directory var keyPathTable: [String: KeyPath<Directory, Declaration>] }

51.

KeyPathを使った方針 struct Declaration: Identifiable { let id: String let name: String // その他のプロパティ } struct File: Identifiable { var id: String { fullPath } let fullPath: String var declarations: [Declaration] } struct Directory: Identifiable { var id: String { fullPath } let fullPath: String var subdirectories: [Self] var files: [File] } 最終的に、RootDirectoryインスタンスを1つ 生成してSingle Source of Truthとする ディレクトリ、ファイル、宣言の構造を維持 したまま抽出する SwiftSyntaxで抽出する流れは省略 struct RootDirectory { var directory: Directory var keyPathTable: [String: KeyPath<Directory, Declaration>] }

52.
[beta]
KeyPathを使った方針
USRを受け取ってDeclarationインスタンスを返すメソッドを実装することで
KeyPathのDictionaryをモジュール内に隠蔽できる
public struct RootDirectory {
var directory: Directory
var keyPathTable: [String: KeyPath<Directory, Declaration>]

}

public func getDeclaration(_ usr: String) -> Declaration? {
guard let toDeclaration = keyPathTable[usr] else {
return nil
}
return directory[keyPath: toDeclaration]
}

53.

各DeclarationまでのKeyPathを 動的に生成する必要がある

54.

KeyPathを動的に生成する

55.

KeyPathを動的に生成する 説明を簡単にするために、Fileから各DeclarationまでのKeyPathを 生成する流れのみを対象とする public struct Declaration: Identifiable { public let id: String let name: String var properties: [Self] // その他のプロパティ } struct File: Identifiable { var id: String { fullPath } let fullPath: String var declarations: [Declaration] }

56.
[beta]
KeyPathを動的に生成する
typealias ToDeclaration = KeyPath<File, Declaration>
typealias KeyPathTable = [String: ToDeclaration]
public struct Declaration: Identifiable {
public let id: String
let name: String
var properties: [Self]
// その他のプロパティ

}

Dictionaryを生成するメソッドを
Declarationに実装する

func generateKeyPath(fromRoot keyPath: ToDeclaration) -> KeyPathTable {
var table: KeyPathTable = [id: keyPath]
for (index, declaration) in properties.enumerated() {
let toChild: ToDeclaration = keyPath.appending(path: \.properties[index])
table.merge(
declaration.generateKeyPath(fromRoot: toChild),
uniquingKeysWith: { current, new in current }
)
}
return table
}

57.
[beta]
KeyPathを動的に生成する
typealias ToDeclaration = KeyPath<File, Declaration>
typealias KeyPathTable = [String: ToDeclaration]
struct File: Identifiable {
var id: String { fullPath }
let fullPath: String
var declarations: [Declaration]

}

Dictionaryを生成するメソッドを
Fileにも実装する

func generateKeyPath() -> KeyPathTable {
var table: KeyPathTable = [:]
for (index, declaration) in declarations.enumerated() {
table.merge(
declaration.generateKeyPath(fromRoot: \.declarations[index]),
uniquingKeysWith: { current, new in current }
)
}
return table
}

58.
[beta]
KeyPathを動的に生成する
typealias ToDeclaration = KeyPath<File, Declaration>
typealias KeyPathTable = [String: ToDeclaration]
struct File: Identifiable {
var id: String { fullPath }
let fullPath: String
var declarations: [Declaration]

インデックスを含めたKeyPathを生成
してDeclarationのメソッドを呼び出す
インデックスが必要なので親から渡す

}

func generateKeyPath() -> KeyPathTable {
var table: KeyPathTable = [:]
for (index, declaration) in declarations.enumerated() {
table.merge(
declaration.generateKeyPath(fromRoot: \.declarations[index]),
uniquingKeysWith: { current, new in current }
)
}
return table
}

59.
[beta]
KeyPathを動的に生成する
typealias ToDeclaration = KeyPath<File, Declaration>
typealias KeyPathTable = [String: ToDeclaration]
public struct Declaration: Identifiable {
public let id: String
let name: String
var properties: [Self]
// その他のプロパティ

}

Fileから受け取ったKeyPathで
Dictionaryを初期化する

func generateKeyPath(fromRoot keyPath: ToDeclaration) -> KeyPathTable {
var table: KeyPathTable = [id: keyPath]
for (index, declaration) in properties.enumerated() {
let toChild: ToDeclaration = keyPath.appending(path: \.properties[index])
table.merge(
declaration.generateKeyPath(fromRoot: toChild),
uniquingKeysWith: { current, new in current }
)
}
return table
}

60.
[beta]
KeyPathを動的に生成する
typealias ToDeclaration = KeyPath<File, Declaration>
typealias KeyPathTable = [String: ToDeclaration]
public struct Declaration: Identifiable {
public let id: String
let name: String
var properties: [Self]
// その他のプロパティ

}

ネストしたpropertiesについて同じ
メソッドを再帰的に呼び出す

func generateKeyPath(fromRoot keyPath: ToDeclaration) -> KeyPathTable {
var table: KeyPathTable = [id: keyPath]
for (index, declaration) in properties.enumerated() {
let toChild: ToDeclaration = keyPath.appending(path: \.properties[index])
table.merge(
declaration.generateKeyPath(fromRoot: toChild),
uniquingKeysWith: { current, new in current }
)
}
return table
}

61.
[beta]
KeyPathを動的に生成する
typealias ToDeclaration = KeyPath<File, Declaration>
typealias KeyPathTable = [String: ToDeclaration]
public struct Declaration: Identifiable {
public let id: String
let name: String
var properties: [Self]
// その他のプロパティ

}

インデックスを含むKeyPathを
継ぎ足して渡す

func generateKeyPath(fromRoot keyPath: ToDeclaration) -> KeyPathTable {
var table: KeyPathTable = [id: keyPath]
for (index, declaration) in properties.enumerated() {
let toChild: ToDeclaration = keyPath.appending(path: \.properties[index])
table.merge(
declaration.generateKeyPath(fromRoot: toChild),
uniquingKeysWith: { current, new in current }
)
}
return table
}

62.

KeyPathを動的に生成する File File File.declarations[0] KeyPathは継ぎ足せる Declaration Declaration.properties[0] File.declarations[0] Declaration.properties[0] + var toDeclaration: KeyPath<File, Declaration> = \.declarations[0] let toProperty: KeyPath<Declaration, Declaration> = \.properties[0] toDeclaration.appending(path: toProperty) // File.declarations[0].properties[0]

63.
[beta]
KeyPathを動的に生成する
typealias ToDeclaration = KeyPath<File, Declaration>
typealias KeyPathTable = [String: ToDeclaration]
public struct Declaration: Identifiable {
public let id: String
let name: String
var properties: [Self]
// その他のプロパティ

}

返ってきたDictionaryを
mergeして返す

func generateKeyPath(fromRoot keyPath: ToDeclaration) -> KeyPathTable {
var table: KeyPathTable = [id: keyPath]
for (index, declaration) in properties.enumerated() {
let toChild: ToDeclaration = keyPath.appending(path: \.properties[index])
table.merge(
declaration.generateKeyPath(fromRoot: toChild),
uniquingKeysWith: { current, new in current }
)
}
return table
}

64.

KeyPathを動的に生成する let childID = "childID" let child = Declaration(id: childID, name: "childProperty") let parentID = "parentID" let parent = Declaration( id: parentID, name: "parent", properties: [child] ) USRに一致するKeyPathをDictionaryから取り出して Fileの[keyPath:]サブスクリプトに渡すことで、 Declarationにアクセスできる var file = File(fullPath: "/path/to/file", declarations: [parent]) let table = file.generateKeyPath() if let toChild = table[childID] { file[keyPath: toChild].name // "childProperty" }

65.
[beta]
KeyPathを動的に生成する
typealias ToDeclaration = WritableKeyPath<File, Declaration>
let childID = "childID"
let child = Declaration(id: childID, name: "childProperty")
let parentID = "parentID"
let parent = Declaration(
id: parentID,
name: "parent",
properties: [child]
)

KeyPathをWritableKeyPathに変えると、
Declarationの更新もできるようになる

var file = File(fullPath: "/path/to/file", declarations: [parent])
let table = file.generateKeyPath()
if let toChild = table[childID] {
file[keyPath: toChild].properties.append(
Declaration(id: "newChildID", name: "newChildProperty")
)
}

66.

まとめ

67.

まとめ • KeyPathはArrayやOptional、Dictionaryも扱える • DictionaryでKeyPathを保持することで再帰的な構造体にアクセス しやすくできる • KeyPathは継ぎ足せる Swiftの強力な機能を活用していきましょう💪