イベント詳細表示と日付変更検知によるカレンダーのUX向上

- イベント行にポップアップによる詳細ビュー(EventDetailView)を追加し、イベントの場所・メモ・URLなどをリッチに表示
- ボタン化によりイベントタップ時の操作性を改善
- NotificationCenterでNSCalendarDayChangedに対応し、日付変更時にイベントリストを自動更新するよう拡張
- 日時や区切りの表示、日本語ロケールへの対応などイベント詳細情報のUIを最適化
This commit is contained in:
2025-10-24 23:30:27 +09:00
parent f3a196c95d
commit b8dd3e254d
2 changed files with 130 additions and 21 deletions

View File

@@ -27,6 +27,14 @@ class CalendarService: ObservableObject {
) { [weak self] _ in
self?.handleEventStoreChanged()
}
NotificationCenter.default.addObserver(
forName: .NSCalendarDayChanged,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleDayChanged()
}
}
@MainActor
@@ -35,6 +43,11 @@ class CalendarService: ObservableObject {
loadEvents()
}
@MainActor
private func handleDayChanged() {
loadEvents()
}
@MainActor
func requestAccess() async {
do {

View File

@@ -126,34 +126,45 @@ struct DayGroup {
struct EventRow: View {
let event: EKEvent
@State private var showingDetail = false
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))
Button(action: {
showingDetail = true
}) {
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)
}
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)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.popover(isPresented: $showingDetail) {
EventDetailView(event: event)
}
.padding(.vertical, 4)
}
private func timeString(from start: Date, to end: Date) -> String {
@@ -162,3 +173,88 @@ struct EventRow: View {
return "\(formatter.string(from: start)) - \(formatter.string(from: end))"
}
}
struct EventDetailView: View {
let event: EKEvent
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
if let calendar = event.calendar {
Circle()
.fill(Color(calendar.color))
.frame(width: 12, height: 12)
Text(calendar.title)
.font(.caption)
.foregroundColor(.secondary)
}
}
Text(event.title ?? "無題のイベント")
.font(.headline)
Divider()
VStack(alignment: .leading, spacing: 8) {
if event.isAllDay {
HStack {
Image(systemName: "clock")
.foregroundColor(.secondary)
.frame(width: 20)
Text("終日")
}
} else {
HStack(alignment: .top) {
Image(systemName: "clock")
.foregroundColor(.secondary)
.frame(width: 20)
VStack(alignment: .leading, spacing: 2) {
Text(formatDateTime(event.startDate))
Text("")
.foregroundColor(.secondary)
.font(.caption)
Text(formatDateTime(event.endDate))
}
}
}
if let location = event.location, !location.isEmpty {
HStack(alignment: .top) {
Image(systemName: "location")
.foregroundColor(.secondary)
.frame(width: 20)
Text(location)
}
}
if let notes = event.notes, !notes.isEmpty {
HStack(alignment: .top) {
Image(systemName: "note.text")
.foregroundColor(.secondary)
.frame(width: 20)
Text(notes)
.textSelection(.enabled)
}
}
if let url = event.url {
HStack(alignment: .top) {
Image(systemName: "link")
.foregroundColor(.secondary)
.frame(width: 20)
Link(url.absoluteString, destination: url)
}
}
}
}
.padding()
.frame(minWidth: 300, maxWidth: 400)
}
private func formatDateTime(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "M月d日(E) HH:mm"
formatter.locale = Locale(identifier: "ja_JP")
return formatter.string(from: date)
}
}