イベント詳細表示と日付変更検知によるカレンダーのUX向上
- イベント行にポップアップによる詳細ビュー(EventDetailView)を追加し、イベントの場所・メモ・URLなどをリッチに表示 - ボタン化によりイベントタップ時の操作性を改善 - NotificationCenterでNSCalendarDayChangedに対応し、日付変更時にイベントリストを自動更新するよう拡張 - 日時や区切りの表示、日本語ロケールへの対応などイベント詳細情報のUIを最適化
This commit is contained in:
@@ -27,6 +27,14 @@ class CalendarService: ObservableObject {
|
|||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
self?.handleEventStoreChanged()
|
self?.handleEventStoreChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: .NSCalendarDayChanged,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.handleDayChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -35,6 +43,11 @@ class CalendarService: ObservableObject {
|
|||||||
loadEvents()
|
loadEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func handleDayChanged() {
|
||||||
|
loadEvents()
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func requestAccess() async {
|
func requestAccess() async {
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -126,34 +126,45 @@ struct DayGroup {
|
|||||||
|
|
||||||
struct EventRow: View {
|
struct EventRow: View {
|
||||||
let event: EKEvent
|
let event: EKEvent
|
||||||
|
@State private var showingDetail = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
Button(action: {
|
||||||
HStack {
|
showingDetail = true
|
||||||
if event.isAllDay {
|
}) {
|
||||||
Text("終日")
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
.font(.caption)
|
HStack {
|
||||||
.foregroundColor(.secondary)
|
if event.isAllDay {
|
||||||
} else {
|
Text("終日")
|
||||||
Text(timeString(from: event.startDate, to: event.endDate))
|
.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)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.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 {
|
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))"
|
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