イベント詳細表示と日付変更検知によるカレンダーのUX向上
- イベント行にポップアップによる詳細ビュー(EventDetailView)を追加し、イベントの場所・メモ・URLなどをリッチに表示 - ボタン化によりイベントタップ時の操作性を改善 - NotificationCenterでNSCalendarDayChangedに対応し、日付変更時にイベントリストを自動更新するよう拡張 - 日時や区切りの表示、日本語ロケールへの対応などイベント詳細情報のUIを最適化
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user