Swift Playground4「予定表」Appプロジェクトを解説する

# この投稿はなに？
iPadとMac向けアプリの「Swift Playgrounds4」に用意されている「予定表」Appプロジェクトを学ぶための解説です。

#「予定表」Appプロジェクト
このプロジェクトでは、イベントを日付ごとに整理してスケジュールを立てる「DatePlanner」アプリを開発します。
このアプリでは、リストビューとオブザーバ・データモデルを使って、イベントやタスクを動的に生成するリストを構築します。
そのために、アプリのビュー階層全体から利用できる「一貫性のあるデータオブジェクト」を作成します。

![IMG_0525.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/fd694464-ef98-118f-9d35-123985e71a4a.jpeg)

# リソースファイル
プロジェクトに含まれるファイルは、機能的な視点から「データモデル（イベントとタスク）、ビュー、シンボルその他」に分けて捉えることができます。

データモデル

- DatePlannerApp
- EventData
- Event
- EventTask

ビュー

- EventList
- EventRow
- EventDetail
- EventEditor 
- TaskRow

シンボルその他

- SymbolPicker
- EventSymbols
- SFSymbolStyling
- ColorOption

## DatePlannerApp.swift


```swift
import SwiftUI

@main
struct DatePlannerApp: App {
    @StateObject private var eventData = EventData()

    var body: some Scene {
        WindowGroup {
            NavigationView {
                EventList()
                Text("Select an Event")
                    .foregroundStyle(.secondary)
            }
            .environmentObject(eventData)

        }
    }
}
```

アプリ内のビュー間を往来できるようにするため、ビュー階層の最上位にナビゲーションコンテナを配置します。
このアプリを起動すると、最初に`EventList`ビューがナビゲーションの起点として表示されます。

アプリに対して画面幅が十分に確保されている場合、`EventList`ビューはサイドバー形式で表示されます。
![IMG_0524.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/9681bc88-94e1-7174-0e11-b2b5ed61029d.jpeg)

ユーザがサイドバーのイベント行事を選択するまでは、`Text`ビューが表示されるようにしています。
ユーザがイベント行事を選択すると、その`EventDetail`ビューがプライマリパネルに表示されます。

イベント行事のデータは、`eventData`プロパティに保持されます。
この`EventData`型プロパティは`@StateObject`属性でマークされるので、オブザーバブル・オブジェクトのインスタンスを作成します。
したがって、この値の変化はSwiftUIフレームワークによって追跡され、依存しているビューの表示を自動的に更新できます。

ここでは、ビュー階層全体で`eventData`プロパティの値を利用するために、`.environmentObject`モディファイアを使って最上位階層のビューにeventData`インスタンスを渡します。

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/0b76714e-5b50-ed5a-9725-9c9a7c2a6c48.jpeg">

## Event.swift
このアプリでは、旅行やパーティーなどのイベント行事を`Event`型オブジェクトのコレクションとして整理します。
各イベントには「シンボル、色、タイトル、タスク（やること）のリスト、日付」などの情報が含まれます。

```swift:Event.swift
import SwiftUI

struct Event: Identifiable, Hashable {
    var id = UUID()
    var symbol: String = EventSymbols.randomName()
    var color: Color = ColorOptions.random()
    var title = ""
    var tasks = [EventTask(text: "")]
    var date = Date()

    var remainingTaskCount: Int {
        tasks.filter { !$0.isCompleted }.count
    }
    
    var isComplete: Bool {
        tasks.allSatisfy { $0.isCompleted }
    }

    var isPast: Bool {
        date < Date.now
    }
    
    var isWithinSevenDays: Bool {
        !isPast && date < Date.now.sevenDaysOut
    }
    
    var isWithinSevenToThirtyDays: Bool {
        !isPast && !isWithinSevenDays && date < Date.now.thirtyDaysOut
    }
    
    var isDistant: Bool {
        date >= Date().thirtyDaysOut
    }

    static var example = Event(
        symbol: "case.fill",
        title: "Sayulita Trip",
        tasks: [
            EventTask(text: "Buy plane tickets"),
            EventTask(text: "Get a new bathing suit"),
            EventTask(text: "Find an airbnb"),
        ],
        date: Date(timeIntervalSinceNow: 60 * 60 * 24 * 365 * 1.5))
}

// 日付操作を行う便宜上のメソッド
extension Date {
    var sevenDaysOut: Date {
        Calendar.autoupdatingCurrent.date(byAdding: .day, value: 7, to: self) ?? self
    }
    
    var thirtyDaysOut: Date {
        Calendar.autoupdatingCurrent.date(byAdding: .day, value: 30, to: self) ?? self
    }
}
```

### Event構造体
`Event`型を`Identifiable`プロトコルに準拠させることで、イベントのリストを作成する際に各行事が識別できるようになります。

リストの各セクションに表示される項目は、計算プロパティが算出した値に基づいて動的に変化します。
`isPast`計算プロパティは「イベントの日付が過去のものかどうか」を算出します。
`remainingTaskCount`計算プロパティは、イベントに含まれている「未完了なタスク」の数です。
`isComplete`プロパティは、イベントに含まれるすべてのタスクが「完了済みかどうか」の真偽値です。

### Calendar構造体
日付の計算や比較のための機能を提供するデータ型です。
`Calendar`型は「時間にまつわる計算をどのように行うか」をカプセル化しています。
カレンダーに関する情報を提供したり、与えられたカレンダー単位の範囲を決定したり、与えられた絶対時間に単位を追加するなどの機能を提供します。

#### `autoupdatingCurrent`プロパティ（型プロパティ）
ユーザーの希望するカレンダーの変更を追跡するカレンダーです。
変異した場合、このカレンダーはユーザーの希望するカレンダーを追跡しなくなる。
自動更新されるカレンダーは、他の自動更新されるカレンダーと比較されるだけです。

#### `date(byAdding:value:to:wrappingComponents:)`メソッド
指定された日付に「指定された成分の量」を加算して、計算された新しい日付の`Date`型インスタンスを返します。

パラメータ

- `components`
日付に追加する値のセットです。

- `value`
指定したコンポーネントに加算する量です。

- `date`
開始日です。

- `wrappingComponents`
`true`の場合、コンポーネントは増分され、オーバーフロー時に0/1にラップアラウンドし、より高次のコンポーネントが増分される原因にならないようにします。デフォルトは`false`なので、メソッド呼び出し時に省略できますす。

返り値
与えられた入力で日付が計算できなかった場合は`nil`を返します。

## EventTask.swift
各イベント行事には、「タスク（やること）のリスト」があります。
各タスクは、`EventTask`構造体として定義されます。

```swift:
import Foundation

struct EventTask: Identifiable, Hashable {
    var id = UUID()
    var text: String
    var isCompleted = false
    var isNew = false
}
```

`Event`型と同様に、`EventTask`型もリスト内で識別できるようにするため、`Identifiable`プロトコルに準拠します。
タスクには、「説明文、完了フラグ、新規フラグ」を示す情報があります。
`Event`型インスタンスは「タスクの完了フラグ」を数えて、未完了タスクの数を追跡します。

## EventData.swift
`EventData`型は、DatePlannerアプリのモデルデータとなるオブジェクトであり、すべてのイベントを格納および変更します。
そのために、`EventData`型は`ObservableObject`プロトコルに準拠しています。
また、`events`プロパティは`@Published`属性でマークされており、この公開値に依存するSwiftUIビューは自動的に表示が更新されます。

```swift:
import SwiftUI

class EventData: ObservableObject {
    @Published var events: [Event] = [
        Event(symbol: "gift.fill",
              color: .red,
              title: "Maya's Birthday",
              tasks: [EventTask(text: "Guava kombucha"),
                      EventTask(text: "Paper cups and plates"),
                      EventTask(text: "Cheese plate"),
                      EventTask(text: "Party poppers"),
                     ],
              date: Date.roundedHoursFromNow(60 * 60 * 24 * 30)),
        Event(symbol: "theatermasks.fill",
              color: .yellow,
              title: "Pagliacci",
              tasks: [EventTask(text: "Buy new tux"),
                      EventTask(text: "Get tickets"),
                      EventTask(text: "Pick up Carmen at the airport and bring her to the show"),
                     ],
              date: Date.roundedHoursFromNow(60 * 60 * 22)),
        Event(symbol: "facemask.fill",
              color: .indigo,
              title: "Doctor's Appointment",
              tasks: [EventTask(text: "Bring medical ID"),
                      EventTask(text: "Record heart rate data"),
                     ],
              date: Date.roundedHoursFromNow(60 * 60 * 24 * 4)),
        Event(symbol: "leaf.fill",
              color: .green,
              title: "Camping Trip",
              tasks: [EventTask(text: "Find a sleeping bag"),
                      EventTask(text: "Bug spray"),
                      EventTask(text: "Paper towels"),
                      EventTask(text: "Food for 4 meals"),
                      EventTask(text: "Straw hat"),
                     ],
              date: Date.roundedHoursFromNow(60 * 60 * 36)),
        Event(symbol: "gamecontroller.fill",
              color: .cyan,
              title: "Game Night",
              tasks: [EventTask(text: "Find a board game to bring"),
                      EventTask(text: "Bring a desert to share"),
                     ],
              date: Date.roundedHoursFromNow(60 * 60 * 24 * 2)),
        Event(symbol: "graduationcap.fill",
              color: .primary,
              title: "First Day of School",
              tasks: [
                  EventTask(text: "Notebooks"),
                  EventTask(text: "Pencils"),
                  EventTask(text: "Binder"),
                  EventTask(text: "First day of school outfit"),
              ],
              date: Date.roundedHoursFromNow(60 * 60 * 24 * 365)),
        Event(symbol: "book.fill",
              color: .purple,
              title: "Book Launch",
              tasks: [
                  EventTask(text: "Finish first draft"),
                  EventTask(text: "Send draft to editor"),
                  EventTask(text: "Final read-through"),
              ],
              date: Date.roundedHoursFromNow(60 * 60 * 24 * 365 * 2)),
        Event(symbol: "globe.americas.fill",
              color: .gray,
              title: "WWDC",
              tasks: [
                  EventTask(text: "Watch Keynote"),
                  EventTask(text: "Watch What's new in SwiftUI"),
                  EventTask(text: "Go to DT developer labs"),
                  EventTask(text: "Learn about Create ML"),
              ],
              date: Date.from(month: 6, day: 7, year: 2021)),
        Event(symbol: "case.fill",
              color: .orange,
              title: "Sayulita Trip",
              tasks: [
                  EventTask(text: "Buy plane tickets"),
                  EventTask(text: "Get a new bathing suit"),
                  EventTask(text: "Find a hotel room"),
              ],
              date: Date.roundedHoursFromNow(60 * 60 * 24 * 19)),
    ]

    func delete(_ event: Event) {
        events.removeAll { $0.id == event.id }
    }
    
    func add(_ event: Event) {
        events.append(event)
    }
    
    func exists(_ event: Event) -> Bool {
        events.contains(event)
    }
    
    func sortedEvents(period: Period) -> Binding<[Event]> {
        Binding<[Event]>(
            get: {
                self.events
                    .filter {
                        switch period {
                        case .nextSevenDays:
                            return $0.isWithinSevenDays
                        case .nextThirtyDays:
                            return $0.isWithinSevenToThirtyDays
                        case .future:
                            return $0.isDistant
                        case .past:
                            return $0.isPast
                        }
                    }
                    .sorted { $0.date < $1.date }
            },
            set: { events in
                for event in events {
                    if let index = self.events.firstIndex(where: { $0.id == event.id }) {
                        self.events[index] = event
                    }
                }
            }
        )
    }
}

enum Period: String, CaseIterable, Identifiable {
    case nextSevenDays = "Next 7 Days"
    case nextThirtyDays = "Next 30 Days"
    case future = "Future"
    case past = "Past"
    
    var id: String { self.rawValue }
    var name: String { self.rawValue }
}

extension Date {
    static func from(month: Int, day: Int, year: Int) -> Date {
        var dateComponents = DateComponents()
        dateComponents.year = year
        dateComponents.month = month
        dateComponents.day = day
        
        let calendar = Calendar(identifier: .gregorian)
        if let date = calendar.date(from: dateComponents) {
            return date
        } else {
            return Date()
        }
    }

    static func roundedHoursFromNow(_ hours: Double) -> Date {
        let exactDate = Date(timeIntervalSinceNow: hours)
        guard let hourRange = Calendar.current.dateInterval(of: .hour, for: exactDate) else {
            return exactDate
        }
        return hourRange.end
    }
}
```

### EventData構造体
`events`プロパティは「`Event`型インスタンスの配列」の変数です。
この変数はオブザーバブル・オブジェクトの公開値なので、配列の要素を操作すると、それに応じてUIが更新されます。

`delete(_:)`メソッド、`add(_:)`メソッド、`exists(_:)`メソッドは、イベントを追加・削除するための機能を提供します。
また、`sortedEvents(period:)`メソッドは、「指定された期間」で並べ替えたイベント配列を返します。（例えば、「今日から7日間分のイベント」など）

### Period列挙型
イベントのリストを並べ替えるために使用する「期間のカテゴリ」です。

## EventList.swift
イベントをリスト形式で表示するためのビューであり、ナビゲーションの起点になります。
この`List`ビューでは、イベントはセクションによってグループごとに分けて表示されます。

```swift:
import SwiftUI

struct EventList: View {
    @EnvironmentObject var eventData: EventData
    @State private var isAddingNewEvent = false
    @State private var newEvent = Event()
    
    var body: some View {
        
        List {
            ForEach(Period.allCases) { period in
                if !eventData.sortedEvents(period: period).isEmpty {
                    Section(content: {
                        ForEach(eventData.sortedEvents(period: period)) { $event in
                            NavigationLink {
                                EventEditor(event: $event)
                            } label: {
                                EventRow(event: event)
                            }
                            .swipeActions {
                                Button(role: .destructive) {
                                    eventData.delete(event)
                                } label: {
                                    Label("Delete", systemImage: "trash")
                                }
                            }
                        }
                    }, header: {
                        Text(period.name)
                            .font(.callout)
                            .foregroundColor(.secondary)
                            .fontWeight(.bold)
                    })
                }
            }
        }
        .navigationTitle("Date Planner")
        .toolbar {
            ToolbarItem {
                Button {
                    newEvent = Event()
                    isAddingNewEvent = true
                } label: {
                    Image(systemName: "plus")
                }
            }
        }
        .sheet(isPresented: $isAddingNewEvent) {
            NavigationView {
                EventEditor(event: $newEvent, isNew: true)
            }
        }
    }
}

struct EventList_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            EventList().environmentObject(EventData())

        }
    }
}
```

このリストビューは`DatePlannerApp`型の定義において、ナビゲーションの最上位ビューとして`.environmentObject`モディファイアを使って`EventData`型インスタンスを受け取っています。
したがって、このリストビューでは`EnvironmentObject`属性の`EventData`型プロパティとして変数を宣言することで、「一貫性のあるモデルデータ」にアクセスできるようになります。

このビューのリストでは、`ForEach`コンテナに「すべての期間のイベント」を対象に反復処理を実行しています。
そして、「対象の期間」ごとにセクションを設けてイベントを表示します。その期間にイベントが存在しなければ、セクションは作成されません。
リスト内の各イベント行事は、それぞれが`EventRow`ビューによって定義されています。

ユーザーがイベントをタップすると、ナビゲーションは画面を`EventEditor`ビューに遷移します。
タップによる画面の遷移は`NavigationLink`を使用しています。

リストのスワイプ操作によるイベント削除を実装するために、`swipeActions`モディファイアを使用しています。
この時、`EventData`型の`delete()`メソッドが呼び出されます。

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/824603b5-da08-fd88-1a04-5aa46ce93463.jpeg">

## EventRow.swift

```swift:
import SwiftUI

struct EventRow: View {
    let event: Event
    
    var body: some View {
        HStack {
            Image(systemName: event.symbol)
                .sfSymbolStyling()
                .foregroundStyle(event.color)
            
            VStack(alignment: .leading, spacing: 5) {
                Text(event.title)
                    .fontWeight(.bold)

                Text(event.date.formatted(date: .abbreviated, time: .shortened))
                    .font(.caption2)
                    .foregroundStyle(.secondary)
            }
            
            if event.isComplete {
                Spacer()
                Image(systemName: "checkmark")
                    .foregroundStyle(.secondary)
            }
            
        }
        .badge(event.remainingTaskCount)
    }
}

struct EventRow_Previews: PreviewProvider {
    static var previews: some View {
        EventRow(event: Event.example)
    }
}
```


<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/ee48af58-964e-8e22-6a67-37ab5934b8d9.jpeg">

## EventDetail.swift

```swift:
import SwiftUI

struct EventDetail: View {
    @Binding var event: Event
    let isEditing: Bool
    
    @State private var isPickingSymbol = false
    
    var body: some View {
        List {

            HStack {
                Button {
                    isPickingSymbol.toggle()
                } label: {
                    Image(systemName: event.symbol)
                        .sfSymbolStyling()
                        .foregroundColor(event.color)
                        .opacity(isEditing ? 0.3 : 1)
                }
                .buttonStyle(.plain)
                .padding(.horizontal, 5)

                if isEditing {
                    TextField("New Event", text: $event.title)
                        .font(.title2)
                } else {
                    Text(event.title)
                        .font(.title2)
                        .fontWeight(.semibold)
                }
            }
            
            if isEditing {
                DatePicker("Date", selection: $event.date)
                    .labelsHidden()
                    .listRowSeparator(.hidden)

            } else {
                HStack {
                    Text(event.date, style: .date)
                    Text(event.date, style: .time)
                }
                .listRowSeparator(.hidden)
            }
            
            Text("Tasks")
                .fontWeight(.bold)
            
            ForEach($event.tasks) { $item in
                TaskRow(task: $item, isEditing: isEditing)
            }
            .onDelete(perform: { indexSet in
                event.tasks.remove(atOffsets: indexSet)
            })
            
            Button {
                event.tasks.append(EventTask(text: "", isNew: true))
            } label: {
                HStack {
                    Image(systemName: "plus")
                    Text("Add Task")
                }
            }
            .buttonStyle(.borderless)
        }
        #if os(iOS)
        .navigationBarTitleDisplayMode(.inline)
        #endif
        .sheet(isPresented: $isPickingSymbol) {
            SymbolPicker(event: $event)
        }
    }
}

struct EventDetail_Previews: PreviewProvider {
    static var previews: some View {
        EventDetail(event: .constant(Event.example), isEditing: true)
    }
}
```


<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/521ad012-eb6b-4524-e425-fbb17c1067d3.jpeg">

## EventEditor.swift

```swift:
import SwiftUI

struct EventEditor: View {
    @Binding var event: Event
    var isNew = false
    
    @State private var isDeleted = false
    @EnvironmentObject var eventData: EventData
    @Environment(\.dismiss) private var dismiss
    
    // Keep a local copy in case we make edits, so we don't disrupt the list of events.
    // This is important for when the date changes and puts the event in a different section.
    @State private var eventCopy = Event()
    @State private var isEditing = false
    
    private var isEventDeleted: Bool {
        !eventData.exists(event) && !isNew
    }
    
    var body: some View {
        VStack {
            EventDetail(event: $eventCopy, isEditing: isNew ? true : isEditing)
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        if isNew {
                            Button("Cancel") {
                                dismiss()
                            }
                        }
                    }
                    ToolbarItem {
                        Button {
                            if isNew {
                                eventData.events.append(eventCopy)
                                dismiss()
                            } else {
                                if isEditing && !isDeleted {
                                    print("Done, saving any changes to \(event.title).")
                                    withAnimation {
                                        event = eventCopy // Put edits (if any) back in the store.
                                    }
                                }
                                isEditing.toggle()
                            }
                        } label: {
                            Text(isNew ? "Add" : (isEditing ? "Done" : "Edit"))
                        }
                    }
                }
                .onAppear {
                    eventCopy = event // Grab a copy in case we decide to make edits.
                }
                .disabled(isEventDeleted)

            if isEditing && !isNew {

                Button(role: .destructive, action: {
                    isDeleted = true
                    dismiss()
                    eventData.delete(event)
                }, label: {
                    Label("Delete Event", systemImage: "trash.circle.fill")
                        .font(.title2)
                        .foregroundColor(.red)
                })
                    .padding()
            }
        }
        .overlay(alignment: .center) {
            if isEventDeleted {
                Color(UIColor.systemBackground)
                Text("Event Deleted. Select an Event.")
                    .foregroundStyle(.secondary)
            }
        }
    }
}

struct EventEditor_Previews: PreviewProvider {
    static var previews: some View {
        EventEditor(event: .constant(Event()))
    }
}
```


<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/c5a95f3e-1176-2bf5-cb4b-bf22310b7e3d.jpeg">

## EventSymbols.swift

```swift:
import Foundation

struct EventSymbols {
    static func randomName() -> String {
        if let random = symbolNames.randomElement() {
            return random
        } else {
            return ""
        }
    }
    
    static func randomNames(_ number: Int) -> [String] {
        var names: [String] = []
        for _ in 0..<number {
            names.append(randomName())
        }
        return names
    }
        
    static var symbolNames: [String] = [
        "house.fill",
        "ticket.fill",
        "gamecontroller.fill",
        "theatermasks.fill",
        "ladybug.fill",
        "books.vertical.fill",
        "moon.zzz.fill",
        "umbrella.fill",
        "paintbrush.pointed.fill",
        "leaf.fill",
        "globe.americas.fill",
        "clock.fill",
        "building.2.fill",
        "gift.fill",
        "graduationcap.fill",
        "heart.rectangle.fill",
        "phone.bubble.left.fill",
        "cloud.rain.fill",
        "building.columns.fill",
        "mic.circle.fill",
        "comb.fill",
        "person.3.fill",
        "bell.fill",
        "hammer.fill",
        "star.fill",
        "crown.fill",
        "briefcase.fill",
        "speaker.wave.3.fill",
        "tshirt.fill",
        "tag.fill",
        "airplane",
        "pawprint.fill",
        "case.fill",
        "creditcard.fill",
        "infinity.circle.fill",
        "dice.fill",
        "heart.fill",
        "camera.fill",
        "bicycle",
        "radio.fill",
        "car.fill",
        "flag.fill",
        "map.fill",
        "figure.wave",
        "mappin.and.ellipse",
        "facemask.fill",
        "eyeglasses",
        "tram.fill",
    ]
}
```

## SFSymbolStyling.swift

```swift:
import SwiftUI

struct SFSymbolStyling: ViewModifier {
    func body(content: Content) -> some View {
        content
            .imageScale(.large)
            .symbolRenderingMode(.monochrome)
    }
}

extension View {
    func sfSymbolStyling() -> some View {
        modifier(SFSymbolStyling())
    }
}
```

## SymbolPicker.swift

```swift:
import SwiftUI

struct SymbolPicker: View {
    @Binding var event: Event
    @State private var selectedColor: Color = ColorOptions.default
    @Environment(\.dismiss) private var dismiss
    @State private var symbolNames = EventSymbols.symbolNames
    @State private var searchInput = ""
    
    var columns = Array(repeating: GridItem(.flexible()), count: 6)

    var body: some View {
        VStack {
            HStack {
                Spacer()
                Button {
                    dismiss()
                } label: {
                    Text("Done")
                }
                .padding()
            }
            HStack {
                Image(systemName: event.symbol)
                    .font(.title)
                    .imageScale(.large)
                    .foregroundColor(selectedColor)

            }
            .padding()

            HStack {
                ForEach(ColorOptions.all, id: \.self) { color in
                    Button {
                        selectedColor = color
                        event.color = color
                    } label: {
                        Circle()
                            .foregroundColor(color)
                    }
                }
            }
            .padding(.horizontal)
            .frame(height: 40)

            Divider()

            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(symbolNames, id: \.self) { symbolItem in
                        Button {
                            event.symbol = symbolItem
                        } label: {
                            Image(systemName: symbolItem)
                                .sfSymbolStyling()
                                .foregroundColor(selectedColor)
                                .padding(5)
                        }
                        .buttonStyle(.plain)
                    }
                }
                .drawingGroup()
            }
        }
        .onAppear {
            selectedColor = event.color
        }
    }
}

struct SFSymbolBrowser_Previews: PreviewProvider {
    static var previews: some View {
        SymbolPicker(event: .constant(Event.example))
    }
}
```


<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/233002e2-545c-1589-c51f-a4941a9cf37c.jpeg">

## TaskRow.swift

```swift:
import SwiftUI

struct TaskRow: View {
    @Binding var task: EventTask
    var isEditing: Bool
    @FocusState private var isFocused: Bool

    var body: some View {
        HStack {
            Button {
                task.isCompleted.toggle()
            } label: {
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
            }
            .buttonStyle(.plain)

            if isEditing || task.isNew {
                TextField("Task description", text: $task.text)
                    .focused($isFocused)
                    .onChange(of: isFocused) { newValue in
                        if newValue == false {
                            task.isNew = false
                        }
                    }

            } else {
                Text(task.text)
            }

            Spacer()
        }
        .padding(.vertical, 10)
        .task {
            if task.isNew {
                isFocused = true
            }
        }
    }
        
}

struct TaskRow_Previews: PreviewProvider {
    static var previews: some View {
        TaskRow(task: .constant(EventTask(text: "Do something!")), isEditing: false)
    }
}
```

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/5b23ba02-56a2-044b-1703-d823d1a1c0f1.jpeg">

## ColorOption.swift

```swift:
import SwiftUI

struct ColorOptions {
    static var all: [Color] = [
        .primary,
        .gray,
        .red,
        .orange,
        .yellow,
        .green,
        .mint,
        .cyan,
        .indigo,
        .purple,
    ]
    
    static var `default` : Color = Color.primary
    
    static func random() -> Color {
        if let element = ColorOptions.all.randomElement() {
            return element
        } else {
            return .primary
        }
        
    }
}
```

<img width=400 src="URL">