トップ 追記

Cocoa練習帳

iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど

2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|

2023-05-02 [Swift]Day One Classicのデータを調べる

Day One Classicという日記アプリを使っていたが、iOSのバージョンが上がって使えなくなったので、このアプリのデータを取り出して、自分の日記アプリの取り込みたいと考え、Day One Classicのデータの書式を調べた。

Journal.dayoneがデータの名前で、これはディレクトリだった。

.
`-- Journal.dayone
    |-- entries
    |   |-- UUID1.doentry
    |   `-- UUIDn.doentry
    `-- photos
        |-- UUID1.jpg
        `-- UUIDm.jpg

日記の本文はentriesディレクトリ配下に、投稿毎の単位でsuffixが.doentryのファイルに記録されている。日記に写真がある場合はphotosディレクトリ配下に対応する.doentryファイルと同じUUIDでJPEGファイルとして格納されている。

日記の本文はプロパティリストの書式となっていて、NSDictionaryとして読み込むことができる。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Creation Date</key>
	<date>2014-01-03T21:40:52Z</date>
	<key>Creator</key>
	<dict>
		<key>Device Agent</key>
		<string>iPhone/iPhone5,2</string>
		<key>Generation Date</key>
		<date>2014-01-03T21:40:52Z</date>
		<key>Host Name</key>
		<string>iPhone5Black64GB</string>
		<key>OS Agent</key>
		<string>iOS/7.0.4</string>
		<key>Software Agent</key>
		<string>Day One iOS/1.12</string>
	</dict>
	<key>Entry Text</key>
	<string>娘と
俺の藤井2014</string>
	<key>Location</key>
	<dict>
		<key>Administrative Area</key>
		<string>埼玉県</string>
		<key>Country</key>
		<string>日本</string>
		<key>Latitude</key>
		<real>35.904324925538063</real>
		<key>Locality</key>
		<string>さいたま市 大宮区</string>
		<key>Longitude</key>
		<real>139.62506669586489</real>
		<key>Place Name</key>
		<string>下町 1丁目2番</string>
	</dict>
	<key>Music</key>
	<dict>
		<key>Track</key>
		<string>Weekend Sunshine - Dec 7, 2013</string>
	</dict>
	<key>Starred</key>
	<false/>
	<key>Time Zone</key>
	<string>Asia/Tokyo</string>
	<key>UUID</key>
	<string>B2713EC2EAF54B64884E8FF85D20DE5F</string>
</dict>
</plist>

Swiftで読み込むコードを書いてみた。

import Foundation
 
func dump(url aUrl: URL) {
    print("dump(\(aUrl))")
    do {
        let urls = try FileManager.default.contentsOfDirectory(
            at: aUrl,
            includingPropertiesForKeys: nil,
            options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
        urls.forEach { url in
            print(url)
            if url.hasDirectoryPath {
                dump(url: url)
            } else {
                if url.pathExtension == "doentry" {
                    let entry = NSDictionary(contentsOfFile: url.path)
                    print("\(String(describing: entry))")
                }
            }
        }
    } catch {
        print(error.localizedDescription)
    }
}
 
let journalDayonePath: String = "/Users/yukio/Documents/Development/Projects/KeepADiary/temp/Day One/Journal.dayone"
let journalDayoneURL = URL(fileURLWithPath: journalDayonePath)
dump(url: journalDayoneURL)

Xcodeのデバッガで値をダンプして、例えば、日付はNSDateのオブジェクトになっていることが確認できた。

_ 【関連情報】

Cocoa練習帳

2023-04-01 [Swift]画面遷移

iOSではシステム準拠の画面遷移が容易に実装できるよう、基本的な画面遷移を実装するためのフレームワークが用意されいてる。

代表的なものを挙げると、以下の通り。

_ ナビゲーションインターフェイス

一方向へ階層構造で画面が遷移するもので、ヘッダー部に前の画面に戻るための方法が用意されている。

ナビゲーションインターフェイス

_ タブ・バー・インターフェイス

異なる系統の画面への切り替わるもの。

タブ・バー・インターフェイス

_ モーダル表示

今ある画面に覆いかぶさるように他の画面が表示されるもので、基本的に、元の画面に戻るもの。

モーダル表示

この画面遷移を簡素なSwiftUIのコードで実装してみることにした。

アプリ本体のクラスは、テンプレートから作成されたもののまま。

import SwiftUI
 
@main
struct DemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

_ ナビゲーションインターフェイス

トップのテーブルの項目をタップすると詳細画面に遷移する。詳細画面からトップのテーブルにはヘッダーの戻るをタップすれば遷移する。

import SwiftUI
 
struct DetailView: View {
    var body: some View {
        Text("Detail View")
    }
}
 
struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                NavigationLink {
                    DetailView()
                } label: {
                    Text("List 01")
                }
                NavigationLink {
                    DetailView()
                } label: {
                    Text("List 02")
                }
            }
            .navigationBarTitle("ナビゲーション")
        }
    }
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

_ タブ・バー・インターフェイス

下部に4つのタブが表示されていて、タップすると画面が切り替わる。

import SwiftUI
 
struct DetailView: View {
    var body: some View {
        Text("Detail View: \(Date())")
    }
}
 
struct ContentView: View {
    var body: some View {
        TabView {
            DetailView()
                .tabItem {
                    Text("Tab 01")
                }
            DetailView()
                .tabItem {
                    Text("Tab 02")
                }
            DetailView()
                .tabItem {
                    Text("Tab 03")
                }
            DetailView()
                .tabItem {
                    Text("Tab 04")
                }
        }
    }
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

_ モーダル表示

中央のボタンをタップするとシートが表示される。シートの中央にもボタンがあるのでタップすると閉じて元の画面に戻る。

import SwiftUI
 
struct SheetView: View {
    @Binding var isShowingSheet: Bool
    var body: some View {
        Button {
            self.isShowingSheet = false
        } label: {
            Text("Close")
        }
    }
}
 
struct ContentView: View {
    @State private var isShowingSettingsSheet: Bool = false
    var body: some View {
        Button(action: {
            self.isShowingSettingsSheet = true
        }) {
            Text("Open SheetView")
        }
        .sheet(isPresented: $isShowingSettingsSheet) {
            SheetView(isShowingSheet: self.$isShowingSettingsSheet)
        }
    }
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

2023-03-09 [Swift]Concurrency

いわゆる、async/awaitのお話だ。

非同期関数 listPhotos() を同期で呼び出して値を受け取る。

import Foundation
 
func listPhotos(inGallery name: String) async -> [String] {
    do {
        _ = try await Task.sleep(nanoseconds: UInt64(1 * 1_000_000_000)) // 1秒待つ
    } catch {}
    return ["photoNames"]
}
 
Task {
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    print("\(photoNames)")
}

async 宣言すると関数は非同期となる。非同期関数を await で呼び出すと、結果が返るまで待つ。

非同期関数 listPhotos() を非同期で呼び出して、結果が返るのを待って、値を受け取る。async / await なコードは、非同期関数、または、@main で記された構造体、クラス、列挙型の static main()、Task からでしか呼び出せない。

import Foundation
 
func listPhotos(inGallery name: String) async -> [String] {
    do {
        _ = try await Task.sleep(nanoseconds: UInt64(1 * 1_000_000_000)) // 1秒待つ
    } catch {}
    return ["photoNames"]
}
 
Task {
    async let photoNames = listPhotos(inGallery: "Summer Vacation")
    print("\(await photoNames)")
}

非同期関数の戻り値を async let で代入すると、非同期で実行される。その戻り値を await をつけて参照すると、結果が返ってくるまで待つ。

非同期関数を反復処理する例。それには、対象がAsyncSequenceプロトコルに準拠している必要がある。

struct Demo {
    struct AsyncDemo: AsyncSequence {
        typealias Element = String
        let num: Int
        
        struct AsyncIterator: AsyncIteratorProtocol {
            var num: Int
            mutating func next() async -> Element? {
                num = num + 1
                return num.description
            }
        }
        
        func makeAsyncIterator() -> AsyncIterator {
            return AsyncIterator(num: num)
        }
    }
    
    func getStr(num: Int) -> AsyncDemo {
        return AsyncDemo(num: num)
    }
}

すると、このように反復処理が記述できる。

Task {
    let demo = Demo()
    for await str in demo.getStr(num: 0) {
        print(str)
        _ = try await Task.sleep(nanoseconds: UInt64(1 * 1_000_000_000)) // 1秒待つ
    }
}

2023-02-26 [Swift]Container view

iOS5からUIViewControllerにビュー・コントローラの親子関係が構築できるコンテナ機能が追加された。当初はさまざまなコードを記述する必要があった。以下は、当時の記事だ。

現在では、StoryboardでContainer viewを配置すると自動で子ビュー・コントローラを生成してくれるようになった。

Container view

2023-01-30 [macOS][iOS]アプリケーション識別子

アプリケーションを識別するIDとは何か?それは、世界でユニークなIDなのか?開発者(Developer Program)でユニークなIDなのか? ストアでユニークなIDなのか?著名なアプリのIDを自分のアプリにつけれるのか?アプリ内課金の不正対策で、レシートが他のアプリのものかどうかを何で確認すればいいのか?一般的なファイルフォーマット(例えばテキストやPNG)のファイルをダブルクリックすると、それを作成したアプリが起動するのは何故か?

ちょっと気になるが知らないと困ることがあるアプリケーション識別子ついて、懐かしい情報から最近の情報まで調査した。

_ ファイルタイプとクリエータ

Macintosh Systemでは4文字の数値('PTNG'や'APPL'など)で種類を表していた。ファイルの種類はファイルタイプ、アプリケーションの識別はクリエータと呼ばれ、重複を防ぐため、APDA(Apple Programmer's and Developer's Association)への登録制度を設けていた。

種類説明
ファイルタイプ 'PTNG'MacPaint書類(PaiNTinG)
'APPL'アプリケーション
'TEXT'テキスト・ファイル
クリエータ '8BIM'Photoshop
'WILD'HyperCard
Jedit Ω

上図の設定を選択するとクリエータが保存されるようになり、プレーンテキスト・ファイルのダブルクリックでJedit Ωが立ち上がる。

_ Uniform Type Identifier (UTI)

データを識別する方法は以下のように複数個の種類が存在するが、UTIはシステム内で扱うデータを識別するための一本化された方法。

  • Macintosh Systemのファイルタイプ (OSType)
  • MS-DOSからの拡張子
  • MIMEタイプ

_ App ID

形式は、seed_id.id。seed_idは10文字の「バンドルシードID(Bundle Seed ID)」で、Member CenterのアカウントIDに基づいて決定される。idはバンドルID。

タイプApp IDバンドルID
明示的123456789A.com.example.Democom.example.Demo
ワイルドカード123456789A.*com.example.Demo
123456789A.com.example.*com.example.Demo

_ Apple DeveloperサイトのApp ID

「証明書、ID、プロファイル」のIdentifiersで登録できるバンドルIDは、ストアでの重複を許さない仕様となっている。

_ Universal links の associated domains

以下は設定ファイルの例。

{
  "applinks": {
      "details": [
           {
             "appIDs": [ "ABCDE12345.com.example.app", "ABCDE12345.com.example.app2" ],
             "components": [
               ...
             ]
           }
       ]
   },
   "webcredentials": {
      "apps": [ "ABCDE12345.com.example.app" ]
   },
    "appclips": {
        "apps": ["ABCED12345.com.example.MyApp.Clip"]
    }
}

appIDsの書式は以下のとおり。

.

App IDだ。

_ StoreKitのレシート

レシートにはApp IDでなくバンドルIDが。 でも、開発者サイトで登録するバンドルIDはユニークなので重複しない。

StoreKit レシート

2022-12-02 [Swift]Combine

Combineはイベント処理演算子を組み合わせて、非同期でイベントを処理するSwiftのAPIだ。
既存のNotificationCenterやAppKitとUIKitのUI部品はCombineに対応しているので、Combineのイベント処理演算子の使ったイベント処理が行える。

例えば、テキスト入力フィールドとデバッグ出力用テキスト・ビューがあるNSViewControllerの派生クラスがあるとする。

import Cocoa
import Combine
 
class ViewController: NSViewController {
    @IBOutlet var inputField: NSTextField!
    @IBOutlet var debugMessageTextView: NSTextView!
    var sub: AnyCancellable? = nil
 
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
 
    override var representedObject: Any? {
        didSet {
            // Update the view, if already loaded.
        }
    }
}

inputFieldにテキストが入力されたら、そのイベントを受け取りたい場合は、以下のコードとなる。

    override func viewDidLoad() {
        super.viewDidLoad()
 
        sub = NotificationCenter.default
            .publisher(for: NSControl.textDidChangeNotification, object: inputField)
            .sink(receiveCompletion: { print ($0) },
                  receiveValue: { print ($0) })
    }

sink() はイベントを受け取るもので、こんな感じでイベント処理演算子をつなげていることになる。

    override func viewDidLoad() {
        super.viewDidLoad()
 
        sub = NotificationCenter.default
            .publisher(for: NSControl.textDidChangeNotification, object: inputField)
            .map( { ($0.object as! NSTextField).stringValue } )
            .filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .receive(on: RunLoop.main)
            .assign(to:\NSTextView.string, on: debugMessageTextView)
    }

上の例のコードを説明する。
map() で文字列に変換している。
filter() で英数字のみとしている。
debounce() でイベント発行の頻度を調整している。
receive() でメイン・スレッドで受け取るようにしている。
assign() で入力内容をデバッグ出力用テキスト・ビューに設定している。


2022-12-01 [macOS][OpenGL]OpenGLに再挑戦(其の参)

前回の続き。

エラーの原因がわかった。glVertexPointer()渡している列挙子が間違えていた。GL_FLATではなくGL_FLOATだ。

#import <iostream>
#import <sstream>
#import <Foundation/Foundation.h>
#import <OpenGL/gl.h>
#import <OpenGL/glu.h>
#import <GLUT/glut.h>
 
int gWidth = 600;
int gHeight = 500;
const int QUIT_VALUE(99);
GLuint  gListID; /* ディスプレイリストID */
 
void display(void)
{
    /* カラーバッファの初期化 */
    glClear(GL_COLOR_BUFFER_BIT);
    
    /* モデリング変換、z軸の負の方向に幾何形状を4単位移動する。 */
    glLoadIdentity();   /* 単位行列 */
    glTranslatef(0.0f, 0.0f, -4.0f);
    
    /* 幾何形状を描画する。 */
    glCallList(gListID);
    
    /* バッファの入れ替え */
    glutSwapBuffers();
    
    assert(glGetError() == GL_NO_ERROR);
}
 
void resize(int w, int h)
{
    /* ウィンドウ・サイズとOpenGLの座標を対応づける */
    glViewport(0, 0, w, h);
    
    /* 投影行列とアスペクト比を更新する */
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(50.0, (GLdouble)w / (GLdouble)h, 1.0, 10.0);
    
    /* 表示ルーチン用にモデルビューモードに設定する */
    glMatrixMode(GL_MODELVIEW);
    
    assert(glGetError() == GL_NO_ERROR);
    
    gWidth = w;
    gHeight = h;
}
 
void keyboard(unsigned char key, int x, int y)
{
    DBGMSG(@"%s", __func__);
}
 
void special(int key, int x, int y)
{
    DBGMSG(@"%s", __func__);
}
 
void mouse(int button, int state, int x, int y)
{
    DBGMSG(@"%s", __func__);
}
 
void motion(int x, int y)
{
    DBGMSG(@"%s", __func__);
}
 
void idle(void)
{
    glutPostRedisplay();
}
 
void main_menu_callback(int value)
{
    if (value == QUIT_VALUE)
        exit(EXIT_SUCCESS);
}
 
void init(void)
{
    DBGMSG(@"%s", __func__);
 
    /* ディザ処理を無効にする */
    glDisable(GL_DITHER);
    
    std::string ver((const char*)glGetString(GL_VERSION));
    assert(! ver.empty());
    std::istringstream verStream(ver);
    
    int major, minor;
    char dummySep;
    verStream >> major >> dummySep >> minor;
    const bool useVertexArrays = ((major >= 1) && (minor >= 1));
    
    const GLfloat data[] = {
        -1.0f, -1.0f, 0.0f,
        1.0f, -1.0f, 0.0f,
        0.0f, 1.0f, 0.0f
    };
    
    if (useVertexArrays) {
        glEnableClientState(GL_VERTEX_ARRAY);
        glVertexPointer(3, GL_FLOAT, 0, data);
    }
    
    /* ディスプレイリストを作成する。 */
    gListID = glGenLists(1);
    glNewList(gListID, GL_COMPILE);
 
    if (useVertexArrays) {
        glDrawArrays(GL_TRIANGLES, 0, 3);
        //glDisableClientState(GL_VERTEX_ARRAY);
    }
    else {
        glBegin(GL_TRIANGLES);
        glVertex3fv(&data[0]);
        glVertex3fv(&data[3]);
        glVertex3fv(&data[6]);
        glEnd();
    }
    
    glEndList();
    
    assert(glGetError() == GL_NO_ERROR);
    
    /* 描画 */
    glutDisplayFunc(display);
 
    /* リサイズ処理 */
    glutReshapeFunc(resize);
    
    /* キーボード */
    glutKeyboardFunc(keyboard);
    
    /* 特殊キー */
    glutSpecialFunc(special);
    
    /* マウス */
    glutMouseFunc(mouse);
    
    /* ドラッグ */
    glutMotionFunc(motion);
    
    /* バックグランド処理 */
    glutIdleFunc(idle);
    
    /* コンテキスト・メニュー */
    glutCreateMenu(main_menu_callback);
    glutAddMenuEntry("Quit", QUIT_VALUE);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
}
 
int main(int argc, const char * argv[])
{
 
    @autoreleasepool {
        
        glutInit(&argc, (char **)argv);
        
        /* RGBカラーモード ダブルバッファ */
        glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
        
        /* 初期ウィンドウ・サイズ */
        glutInitWindowSize(gWidth, gHeight);
        
        /* 初期ウィンドウ位置 */
        glutInitWindowPosition(500, 100);
        
        /* タイトルバー */
        glutCreateWindow("IRIS GL");
        
        /* 初期化 */
        init();
        
        /* 主ループ(イベント駆動) */
        glutMainLoop();
        
    }
    return 0;
}

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/mac/IRIS - GitHub

2022-11-23 [macOS]Mathmaticaを手軽に利用する

Mathmaticaを手軽に無料で利用する方法は、2つあるようだ。一つは、Wolfram Cloudの無料アクセス。もう一つは、Wolfram Engine。それぞれの利用方法を説明する。

_ Wolfram Cloud

Wolfram Cloudの利用を開始する。

  1. Wolfram Cloudの登録ページを開く。
    Wolfram Cloud
  2. "Sign up for free"を選ぶ。
  3. Wolfram IDを作成する。
  4. Wolfram Cloudのトップ・ページで"New Notebook"を選ぶ。
  5. 登録したメールアドレス宛に確認メールが届くので、承認する。

ノートブックで、命令を入力して、Shift+Enterで実行する。

In[1]:= 5 + 7
Out[1]= 12
Wolfram Cloud

_ Wolfram Engine

Wolfram Engineの利用を開始する。

  1. Wolfram Engineのダウンロード・ページを開く。
    Wolfram Engine
  2. ダウンロードしてインストールする。

Wolfram ScriptアイコンをダブルクリックするとTerminal.appが開くので、命令を入力してEnterで実行する。

Wolfram Language 13.1.0 Engine for Mac OS X x86 (64-bit)
Copyright 1988-2022 Wolfram Research, Inc.
 
In[1]:= 5 + 7
 
Out[1]= 12
 
In[2]:=

Wolfram Scriptは、ターミナルでwolframscriptコマンドを実行しても利用できる。

また、スクリプト・ファイルを作成して実行することもできる。

$ cat demo.wls 
#!/usr/bin/env wolframscript
Print[5 + 7]
$ chmod +x demo.wls
$ ./demo.wls
12

ただし、ターミナルでWolfram Scriptを実行する方法では、グラフ表示はできない。

_ ソースコード

GitHubからどうぞ。
https://github.com/murakami/workbook/blob/master/mathematica/demo.wls - GitHub

2022-11-20 [Swift][Kotlin]UInt

プログラミング言語の制限を付与する機能は誰に対してなのか?例えばプライベートに対して隠すというのは、ソースが読める時点で丸見えなような。これについて、相手に対して不必要な情報を見せないという上手い表現をする人もいて、自分の気に入って使わせてもらっている。

これらの制限はなんなのか?注釈で説明する手間を省くためのものなのか?

例えば、C++にはconst地獄という言葉があって、せっかくconstが付けられていても、仕様が変わって、constを外すために多くのファイルで修正が発生しまったり、const値をライブラリに渡そうとすると、受け取り側がconstでないのでキャストすることになってしまったり。

ファイル内でしか使われない変数にconstがついていると、これを書いた人は自分自身が信用できなかったのかなと想像してしまったりする。

また、unsignedはどう考えればいいのか?負の値があり得ない変数はunsignedを選択すべきなのか?0より小さな値にならない制限だとすると上限値の制限はどうするの?下限値にないしてはコンパイラに任せ、上限値はプログラマがif文でチェックするのか?例えば、上限値は100、下限値は10で、どちらもif文でチェックしている場合、コンパイラの0以上チェックななんなのか?

iBooksとWebで公開されているThe Swift Programming Languageを読んでいて、興味深い説明を見つけた。

NOTE
Use UInt only when you specifically need an unsigned integer type with the same size as the platform’s native word size. If this isn’t the case, Int is preferred, even when the values to be stored are known to be nonnegative. A consistent use of Int for integer values aids code interoperability, avoids the need to convert between different number types, and matches integer type inference, as described in Type Safety and Type Inference.>/td>

大雑把に訳すと、特別な理由がない限りUIntを使わずIntを使え。理由はキャストを発生させないため。

厳密は型を気にする人が、構造体のメンバーでInt32やUIntなどを細かく指定するのを見たことがあるが、ライブラリの関数に値を渡す場合は、演算の際にIntにキャストすることになってしまい、それが本当に安全か?というのモヤモヤしてましたが、基本的にIntとすればキャストが発生しないので、ある程、こちらの方が安全ですね。

公式のドキュメントに書いてくれると、周りに説明しやすいので助かる。ありがとう。


2022-10-11 [Swift]Codable

シリアライズのためのプロトコルのEncodableとDecodableがあるが、その両方に対応するのがCodableだ。

typealias Codable = Decodable & Encodable

基本データ型のIntやDouble、StringなどはプロトコルCodableに適合しているので、基本データ型で構成されている場合は以下のように定義する。

struct MyItem: Identifiable, Codable {
    var id = UUID()
    var title: String
}

トップ 追記