【SwiftUI】SwiftData でスレッドセーフにバックグラウンドでデータを扱う 🔄 - @ModelActor

SwiftData は便利だけども UI スレッドが重くならないのかな。

という考えからの Todo アプリを作成しながらのスレッドまわりの実装修行。

 

🔄 実装イメージ

API にリクエストを投げると画面が自動的に更新される、という形。

時間のかかる処理は UI スレッドに負荷がかからないように他のスレッドで処理する。

SwiftUI のボタン押してのダウンロードリクエストから、WEB API からデータ取得して、SwiftData ストレージ に保存。

SwiftUI 上では SwiftData マクロ @Query で observe しておいて変化検出即時画面更新。

【SwiftUI】SwiftData をスレッドセーフにバックグラウンドでデータを扱う 🔄 - @ModelActor

そのまま、SwiftData ストレージはローカルキャッシュとなる。

 

🔄 SwiftData はバックグラウンドで使えるのか

WWDC2023 での会話らしいです。

@Duncan: As I understand it, SwiftData model objects are not thread-safe, just like NSManagedObjects. Are there any additional mechanisms to make managing this easier for us than it was in traditional Core Data? e.g., compiler warnings before passing an object out of its context?

@Dave N (Apple): We have provided the ModelActor protocol & the DefaultModelExecutor to make SwiftData work with Swift Concurrency. Use your ModelContainer to initialize a ModelContext in the initializer for your ModelActor conforming actor object and use that context to initialize a DefaultModelExecutor. This will allow you to use that context with async functions on your actor.

@Ben T (Apple): Swift will enforce sendability requirements

@Duncan: SwiftData のモデルオブジェクトは NSManagedObject と同様にスレッドセーフではないと理解しています。従来の Core Data と比べて、オブジェクトを扱う際に管理しやすくするための追加のメカニズムはありますか?例えば、オブジェクトをそのコンテキストから出す前にコンパイラの警告があるでしょうか?

@Dave N(Apple): 私たちは ModelActor プロトコルと DefaultModelExecutor を提供しており、Swift Concurrency と連携するための SwiftData を使用できます。ModelActor に準拠するアクターオブジェクトのイニシャライザで ModelContainer を使用して ModelContext を初期化し、そのコンテキストを使用して DefaultModelExecutor を初期化します。これにより、そのコンテキストをアクターの非同期関数で使用できるようになります。

@Ben T(Apple): Swift は送信可能性の要件を強制します。

👉 Using SwiftData in background? | Apple Developer Forums hatena-bookmark

1年前ぐらいの会話なので、今では使えそうです。

  • ModelContext
  • ModelActor
  • DefaultModelExecutor

が検索のキーワードでしょうか。

 

🔄 @ModelActor

このマクロを expand すると、前述の会話を実装していることがわかります。

class でなく actor に付けるようです。


@ModelActor
actor SampleService {
  nonisolated let modelExecutor: any SwiftData.ModelExecutor
  nonisolated let modelContainer: SwiftData.ModelContainer

  init(modelContainer: SwiftData.ModelContainer) {
    let modelContext = ModelContext(modelContainer)
    self.modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext)
    self.modelContainer = modelContainer
  }
}

extension SampleService: SwiftData.ModelActor {
}

DefaultSerialModelExecutor() になっているのは、改良して直感的に順番通りに流れるようになったのかな。

ランダムにアドバイスをくれるAPIを利用して実装します。


{
  "slip": { 
    "id": 136, 
    "advice": "Everything matters, but nothing matters that much."
  }
}

👉 Advice Slip JSON API hatena-bookmark

modelContext が非同期関数内で利用できるようになりました。


@ModelActor
actor TodoService {
  private(set) static var shared: TodoService!
  
  static func create(modelContainer: ModelContainer) {
    shared = TodoService(modelContainer: modelContainer)
  }
  
  func download() async {
    do {
      for i in 0 ..< 10 {
        let random = await RandomAdvice.get()
        modelContext.insert(Todo(text: "\(i). \(random.slip.advice)"))
        try modelContext.save()
      }
    } catch {
      if Task.isCancelled {
        print("Task was cancelled.")
      }
    }
  }
}

Task は別ボタンでキャンセルできるように掴んでおく。


Button {
  task = Task {
    await TodoService.shared.download()
  }
} label: {
  Image(systemName: "icloud.and.arrow.down")
}
.onAppear {
  TodoService.create(modelContainer: modelContext.container)
}

 

🔄 まとめ

マクロって GitHub でコードを眺めてるとコードの意味が全くみえない。

しかし、SwiftData のみでここまでできることに驚きました。



恐るべし。

しかし、謎なことが、

👉 【Swift】この ModelActor ってなぜ生きてるの? hatena-bookmark

 

🔄 参考

👉 Context outside of SwiftUI Views | Apple Developer Forums hatena-bookmark
👉 Sendable and @Sendable closures explained with code examples hatena-bookmark
👉 SwiftData background inserts using… | Apple Developer Forums hatena-bookmark
👉 ModelActor Implementation Changes … | Apple Developer Forums hatena-bookmark


【Xcode】Preview Crashed で 表示されない 📲

Xcode の Preview がクラッシュしっぱなし。

【Xcode】Preview Crashed で 表示されない

ストレージまわりの話ではないかと思いながらしらべる。


xcrun simctl -—set previews delete all

👉 Preview keeps crashing | Apple Developer Forums hatena-bookmark
👉 SwiftUIのプレビューが動かなくなった時の対処方法 #SwiftUI - Qiita hatena-bookmark

 

📲 ログから原因を見つける

まずは、見てみますか。


#!/bin/bash

cd ~/Library/Logs/DiagnosticReports
file=$(ls XCPreview* | head -1)
open $file

何か起こったらログ確認大事。

 

📲 対応方法

いろいろありそうです。

このシミュレータですが、通常のシミュレータとは別でXcodeが持っています。しかしストレージ管理のデベロッパ欄には表示されない上に自動で削除されません。私の環境ではiOS14.1のシミュレータなども残っていました。

👉 【SwiftUI】PreviewのSimulator残留 | thwork hatena-bookmark


cd ~/Library/Developer/Xcode/UserData/Previews/Simulator\ Devices/
find . -name com.apple.suggestions.plist -exec plutil -replace SuggestionsAppLibraryEnabled -bool NO {} ";"

👉 [Tips]Xcode13.1のSwiftUIのpreviewでCPU使用率が高くなるのをSpotlightを止めて回避する(iOS15) #iOS - Qiita hatena-bookmark

- Restart the computer and re-run xcrun simctl --set previews delete all
- Delete stored Preview data directly. This data is stored in ~/Library/Developer/Xcode/UserData/Previews.

👉 Use Realm with SwiftUI Previews — Realm hatena-bookmark


xcrun simctl --set previews delete unavailable | all

👉 SwiftUI Previews - Disk space issue | Apple Developer Forums hatena-bookmark

Save space by
1. xcrun simctl delete unavailable
2. xcrun simctl erase all
3. Delete /Users/username/Library/Developer/CoreSimulator folder.

👉 swiftui - Xcode 13 UI Previews folder takes too much space - Stack Overflow hatena-bookmark

 

📲 xcrun simctl とは ?

コマンドラインツールから help を確認する。


❯ xcrun simctl help
usage: simctl [--set <path>] [--profiles <path>] <subcommand> ...
       simctl help [subcommand]
Command line utility to control the Simulator

For subcommands that require a <device> argument, you may specify a device UDID
or the special "booted" string which will cause simctl to pick a booted device.
If multiple devices are booted when the "booted" device is selected, simctl
will choose one of them.

Subcommands:
	addmedia            Add photos, live photos, videos, or contacts to the library of a device.
	boot                Boot a device or device pair.
	clone               Clone an existing device.
	create              Create a new device.
	delete              Delete specified devices, unavailable devices, or all devices.
	diagnose            Collect diagnostic information and logs.
	erase               Erase a device's contents and settings.
	get_app_container   Print the path of the installed app's container
	getenv              Print an environment variable from a running device.
	help                Prints the usage for a given subcommand.
	icloud_sync         Trigger iCloud sync on a device.
	install             Install an app on a device.
	install_app_data    Install an xcappdata package to a device, replacing the current contents of the container.
	io                  Set up a device IO operation.
	keychain            Manipulate a device's keychain
	launch              Launch an application by identifier on a device.
	list                List available devices, device types, runtimes, or device pairs.
	location            Control a device's simulated location
	logverbose          enable or disable verbose logging for a device
	openurl             Open a URL in a device.
	pair                Create a new watch and phone pair.
	pair_activate       Set a given pair as active.
	pbcopy              Copy standard input onto the device pasteboard.
	pbpaste             Print the contents of the device's pasteboard to standard output.
	pbsync              Sync the pasteboard content from one pasteboard to another.
	privacy             Grant, revoke, or reset privacy and permissions
	push                Send a simulated push notification
	rename              Rename a device.
	runtime             Perform operations on runtimes
	shutdown            Shutdown a device.
	spawn               Spawn a process by executing a given executable on a device.
	status_bar          Set or clear status bar overrides
	terminate           Terminate an application by identifier on a device.
	ui                  Get or Set UI options
	uninstall           Uninstall an app from a device.
	unpair              Unpair a watch and phone pair.
	upgrade             Upgrade a device to a newer runtime.

❯ xcrun simctl help delete
Delete specified devices, unavailable devices, or all devices.
Usage: simctl delete <device> [... <device n>] | unavailable | all

Specifying unavailable will delete devices that are not supported by the current Xcode SDK.

❯ xcrun simctl list devices
== Devices ==
-- iOS 17.0 --
    iPhone SE (3rd generation) (EE4D7500-5439-4D41-AD42-6028F649FA44) (Shutdown)
    iPhone 15 (984F204F-2D4E-4E56-8EEE-D11C46EDD9C2) (Shutdown)
    iPhone 15 Plus (C1349C55-57AC-4E47-9EF3-71A848E24A4E) (Shutdown)
    iPhone 15 Pro (04DC04FF-87DB-4EB6-91D8-C8CB4CF10015) (Shutdown)
    iPhone 15 Pro Max (5DC11544-4C3E-4852-8899-3D9E0844519B) (Shutdown)
    iPad Air (5th generation) (906B44B3-B298-4CF1-A5FB-0B4D43277636) (Shutdown)
    iPad (10th generation) (3804C1FA-6306-4CB7-98E0-D24CAFA70537) (Shutdown)
    iPad mini (6th generation) (351ED4C6-2DF3-4F1F-A29A-6D44483D9A95) (Shutdown)
    iPad Pro (11-inch) (4th generation) (96D48442-96AA-421A-BBA6-6F45EE8ECDBE) (Shutdown)
    iPad Pro (12.9-inch) (6th generation) (0B527419-DE2A-4146-93C4-55540B624C5C) (Shutdown)

❯ xcrun simctl --set previews list devices
Using Previews Device Set: '/Users/mao/Library/Developer/Xcode/UserData/Previews/Simulator Devices'
== Devices ==
-- iOS 17.0 --
    iPhone 15 Pro (676858D4-35E8-4841-91F4-17804B3E7565) (Shutdown)

覚えておくべきは、

Preview は、シュミレータ単独と別枠でシュミレータを使っている。

ということ。

 

📲 まとめ

Preview と シュミレーターに関連する問題は多そうです。

  • CPU 使用率
  • ストレージ占有
  • キャッシュ・データ不整合

私の場合スッキリ対応した方法としては、以下を Xcode を一旦終了して実行。


xcrun simctl --set previews delete all
rm -rf ~/Library/Developer/Xcode/UserData/Previews
rm -rf ~/Library/Developer/CoreSimulator/Caches

👉 How to clear/reset SwiftUI Preview Caches? hatena-bookmark

Behavors に入れときますか、消した previews に関しては自動で作成されると思って良さそう。


⌘英かな から Karabiner-Elements へ ⌨️

こうしてましたが。

👉 【macOS】IDE で 矢印 (カーソルキー) を使うと キーボード ホームポジション がずれる件 hatena-bookmark

⌘英かな を卒業して、Karabiner-Elements を使います。

👉 Karabiner-Elementsの本家が「英数/かな」の切り替えに対応したみたい #karabiner-Elements - Qiita hatena-bookmark
👉 「macOS カーソル強制バインディングのすすめ」の最近の様子 - ちなみに hatena-bookmark

 

⌨️ caps lock → control


Simple Modifications

  ↓

For All Devices

  ↓

caps_lock

  ↓

left_control

⌘英かな から Karabiner-Elements へ ⌨️

👉 Karabiner-Elements complex_modifications rules hatena-bookmark

 

⌨️ left / right command → 英数/かな


Complex Modifications

  ↓

Add rule

  ↓

Import more rules from the Internet (Open a web browser)

  ↓

Seearch

⌘英かな から Karabiner-Elements へ ⌨️

👉 [ US ]左右のコマンドキー(⌘)を、単独で押したときは 英数・かな キー として扱う(左⌘は 英数、右⌘は かな) hatena-bookmark

 

⌨️ control (旧 caps lock) + p / n / f / b → ↑ / ↓ / ← / →

⌘英かな から Karabiner-Elements へ ⌨️

👉 Ctrl+p/Ctrl+n to arrow up/down  hatena-bookmark
👉 Map ctrl+b/ctrl+f to left/right arrow  hatena-bookmark

 

⌨️ まとめ

設定保存先は、~/.config/karabiner/karabiner.json

思ったより長い。


{
    "global": {
        "ask_for_confirmation_before_quitting": true,
        "check_for_updates_on_startup": true,
        "show_in_menu_bar": true,
        "show_profile_name_in_menu_bar": false,
        "unsafe_ui": false
    },
    "profiles": [
        {
            "complex_modifications": {
                "parameters": {
                    "basic.simultaneous_threshold_milliseconds": 50,
                    "basic.to_delayed_action_delay_milliseconds": 500,
                    "basic.to_if_alone_timeout_milliseconds": 1000,
                    "basic.to_if_held_down_threshold_milliseconds": 500,
                    "mouse_motion_to_scroll.speed": 100
                },
                "rules": [
                    {
                        "description": "[ US ]左右のコマンドキー(⌘)を、単独で押したときは 英数・かな キー として扱う(左⌘は 英数、右⌘は かな)",
                        "manipulators": [
                            {
                                "description": " LEFT COMMAND → EISUU ",
                                "from": {
                                    "key_code": "left_command",
                                    "modifiers": {
                                        "optional": [
                                            "any"
                                        ]
                                    }
                                },
                                "to": [
                                    {
                                        "key_code": "left_command"
                                    }
                                ],
                                "to_if_alone": [
                                    {
                                        "key_code": "japanese_eisuu"
                                    }
                                ],
                                "type": "basic"
                            },
                            {
                                "description": " RIGHT COMMAND → KANA ",
                                "from": {
                                    "key_code": "right_command",
                                    "modifiers": {
                                        "optional": [
                                            "any"
                                        ]
                                    }
                                },
                                "to": [
                                    {
                                        "key_code": "right_command"
                                    }
                                ],
                                "to_if_alone": [
                                    {
                                        "key_code": "japanese_kana"
                                    }
                                ],
                                "type": "basic"
                            }
                        ]
                    },
                    {
                        "description": "Ctrl+p/Ctrl+n to arrow up/down",
                        "manipulators": [
                            {
                                "from": {
                                    "key_code": "p",
                                    "modifiers": {
                                        "mandatory": [
                                            "control"
                                        ]
                                    }
                                },
                                "to": [
                                    {
                                        "key_code": "up_arrow"
                                    }
                                ],
                                "type": "basic"
                            },
                            {
                                "from": {
                                    "key_code": "n",
                                    "modifiers": {
                                        "mandatory": [
                                            "control"
                                        ]
                                    }
                                },
                                "to": [
                                    {
                                        "key_code": "down_arrow"
                                    }
                                ],
                                "type": "basic"
                            }
                        ]
                    },
                    {
                        "description": "Map ctrl+b/ctrl+f to left/right arrow",
                        "manipulators": [
                            {
                                "from": {
                                    "key_code": "b",
                                    "modifiers": {
                                        "mandatory": [
                                            "control"
                                        ]
                                    }
                                },
                                "to": [
                                    {
                                        "key_code": "left_arrow"
                                    }
                                ],
                                "type": "basic"
                            },
                            {
                                "from": {
                                    "key_code": "f",
                                    "modifiers": {
                                        "mandatory": [
                                            "control"
                                        ]
                                    }
                                },
                                "to": [
                                    {
                                        "key_code": "right_arrow"
                                    }
                                ],
                                "type": "basic"
                            }
                        ]
                    }
                ]
            },
            "devices": [
                {
                    "disable_built_in_keyboard_if_exists": false,
                    "fn_function_keys": [],
                    "identifiers": {
                        "is_keyboard": true,
                        "is_pointing_device": false,
                        "product_id": 0,
                        "vendor_id": 0
                    },
                    "ignore": false,
                    "manipulate_caps_lock_led": true,
                    "simple_modifications": [],
                    "treat_as_built_in_keyboard": false
                },
                {
                    "disable_built_in_keyboard_if_exists": false,
                    "fn_function_keys": [],
                    "identifiers": {
                        "is_keyboard": false,
                        "is_pointing_device": true,
                        "product_id": 0,
                        "vendor_id": 0
                    },
                    "ignore": true,
                    "manipulate_caps_lock_led": false,
                    "simple_modifications": [],
                    "treat_as_built_in_keyboard": false
                }
            ],
            "fn_function_keys": [
                {
                    "from": {
                        "key_code": "f1"
                    },
                    "to": [
                        {
                            "consumer_key_code": "display_brightness_decrement"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f2"
                    },
                    "to": [
                        {
                            "consumer_key_code": "display_brightness_increment"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f3"
                    },
                    "to": [
                        {
                            "apple_vendor_keyboard_key_code": "mission_control"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f4"
                    },
                    "to": [
                        {
                            "apple_vendor_keyboard_key_code": "spotlight"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f5"
                    },
                    "to": [
                        {
                            "consumer_key_code": "dictation"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f6"
                    },
                    "to": [
                        {
                            "key_code": "f6"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f7"
                    },
                    "to": [
                        {
                            "consumer_key_code": "rewind"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f8"
                    },
                    "to": [
                        {
                            "consumer_key_code": "play_or_pause"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f9"
                    },
                    "to": [
                        {
                            "consumer_key_code": "fast_forward"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f10"
                    },
                    "to": [
                        {
                            "consumer_key_code": "mute"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f11"
                    },
                    "to": [
                        {
                            "consumer_key_code": "volume_decrement"
                        }
                    ]
                },
                {
                    "from": {
                        "key_code": "f12"
                    },
                    "to": [
                        {
                            "consumer_key_code": "volume_increment"
                        }
                    ]
                }
            ],
            "name": "Default profile",
            "parameters": {
                "delay_milliseconds_before_open_device": 1000
            },
            "selected": true,
            "simple_modifications": [
                {
                    "from": {
                        "key_code": "caps_lock"
                    },
                    "to": [
                        {
                            "key_code": "left_control"
                        }
                    ]
                }
            ],
            "virtual_hid_keyboard": {
                "country_code": 0,
                "indicate_sticky_modifier_keys_state": true,
                "mouse_key_xy_scale": 100
            }
        }
    ]
}

ありがとう、⌘英かな。

👉 ⌘英かな hatena-bookmark

しかし、Karabiner-Elements の検索見づらいな。