カレンダーイベント管理アプリの最初のプロジェクト構成と基本実装
- .gitignore や project.yml、Makefile などによる基本的なプロジェクト構成ファイルを追加 - macOSカレンダーイベントを表示するSwiftUIアプリの主要コンポーネント(イベント取得・リスト・カレンダー設定画面など)を実装 - カレンダーの表示期間や「今日へスクロール」「設定画面」遷移などの基本動作に対応
This commit is contained in:
164
Sources/ContentView.swift
Normal file
164
Sources/ContentView.swift
Normal file
@@ -0,0 +1,164 @@
|
||||
import SwiftUI
|
||||
import EventKit
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var calendarService = CalendarService()
|
||||
@State private var scrollToToday = false
|
||||
@State private var showingSettings = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
if calendarService.hasAccess {
|
||||
ScrollViewReader { proxy in
|
||||
List {
|
||||
ForEach(groupedEventsByDay(), id: \.date) { dayGroup in
|
||||
Section(header: dayHeader(for: dayGroup.date)) {
|
||||
ForEach(dayGroup.events, id: \.eventIdentifier) { event in
|
||||
EventRow(event: event)
|
||||
}
|
||||
}
|
||||
.id(dayGroup.date)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.onAppear {
|
||||
if !scrollToToday {
|
||||
let today = Calendar.current.startOfDay(for: Date())
|
||||
proxy.scrollTo(today, anchor: .top)
|
||||
scrollToToday = true
|
||||
}
|
||||
}
|
||||
.onChange(of: scrollToToday) { _, newValue in
|
||||
if newValue {
|
||||
let today = Calendar.current.startOfDay(for: Date())
|
||||
withAnimation {
|
||||
proxy.scrollTo(today, anchor: .top)
|
||||
}
|
||||
scrollToToday = false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
Text("カレンダーへのアクセス権限が必要です")
|
||||
.font(.headline)
|
||||
Text("システム設定からアクセスを許可してください")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingSettings) {
|
||||
SettingsView(calendarService: calendarService)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(action: {
|
||||
showingSettings = true
|
||||
}) {
|
||||
Label("設定", systemImage: "gearshape")
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(action: {
|
||||
scrollToToday = true
|
||||
}) {
|
||||
Label("今日", systemImage: "calendar")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func groupedEventsByDay() -> [DayGroup] {
|
||||
let calendar = Calendar.current
|
||||
let grouped = Dictionary(grouping: calendarService.events) { event in
|
||||
calendar.startOfDay(for: event.startDate)
|
||||
}
|
||||
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
guard let startDate = calendar.date(byAdding: .day, value: -14, to: today),
|
||||
let endDate = calendar.date(byAdding: .day, value: 14, to: today) else {
|
||||
return []
|
||||
}
|
||||
|
||||
var allDates: [Date] = []
|
||||
var currentDate = startDate
|
||||
while currentDate <= endDate {
|
||||
allDates.append(currentDate)
|
||||
guard let nextDate = calendar.date(byAdding: .day, value: 1, to: currentDate) else { break }
|
||||
currentDate = nextDate
|
||||
}
|
||||
|
||||
return allDates.map { date in
|
||||
DayGroup(date: date, events: grouped[date] ?? [])
|
||||
}.sorted { $0.date < $1.date }
|
||||
}
|
||||
|
||||
private func dayHeader(for date: Date) -> some View {
|
||||
let calendar = Calendar.current
|
||||
let isToday = calendar.isDateInToday(date)
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "M月d日(E)"
|
||||
formatter.locale = Locale(identifier: "ja_JP")
|
||||
|
||||
return HStack {
|
||||
Text(formatter.string(from: date))
|
||||
.font(.headline)
|
||||
.foregroundColor(isToday ? .blue : .primary)
|
||||
if isToday {
|
||||
Text("今日")
|
||||
.font(.caption)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DayGroup {
|
||||
let date: Date
|
||||
let events: [EKEvent]
|
||||
}
|
||||
|
||||
struct EventRow: View {
|
||||
let event: EKEvent
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
if event.isAllDay {
|
||||
Text("終日")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Text(timeString(from: event.startDate, to: event.endDate))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
if let calendar = event.calendar {
|
||||
Circle()
|
||||
.fill(Color(calendar.color))
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
}
|
||||
Text(event.title ?? "無題のイベント")
|
||||
.font(.body)
|
||||
if let location = event.location, !location.isEmpty {
|
||||
Text(location)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
private func timeString(from start: Date, to end: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
return "\(formatter.string(from: start)) - \(formatter.string(from: end))"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user