diff --git a/app-ios/DroidKaigi2022/DroidKaigi2022.xcodeproj/project.pbxproj b/app-ios/DroidKaigi2022/DroidKaigi2022.xcodeproj/project.pbxproj
index ce4199940..78673cf0f 100644
--- a/app-ios/DroidKaigi2022/DroidKaigi2022.xcodeproj/project.pbxproj
+++ b/app-ios/DroidKaigi2022/DroidKaigi2022.xcodeproj/project.pbxproj
@@ -324,6 +324,7 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iOS/Info.plist;
+ INFOPLIST_KEY_NSCalendarsUsageDescription = "Use for adding event to calendar";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -356,6 +357,7 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iOS/Info.plist;
+ INFOPLIST_KEY_NSCalendarsUsageDescription = "Use for adding event to calendar";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
diff --git a/app-ios/DroidKaigi2022/iOS/Info.plist b/app-ios/DroidKaigi2022/iOS/Info.plist
index f267fdc14..c81e290ae 100644
--- a/app-ios/DroidKaigi2022/iOS/Info.plist
+++ b/app-ios/DroidKaigi2022/iOS/Info.plist
@@ -2,12 +2,12 @@
+ CFBundleLocalizations
+
+ en
+ ja
+
ITSAppUsesNonExemptEncryption
- CFBundleLocalizations
-
- en
- ja
-
diff --git a/app-ios/Package.swift b/app-ios/Package.swift
index f410bada7..61cfcef93 100644
--- a/app-ios/Package.swift
+++ b/app-ios/Package.swift
@@ -69,6 +69,7 @@ var package = Package(
.target(name: "Auth"),
.target(name: "Container"),
.target(name: "ContributorFeature"),
+ .target(name: "Event"),
.target(name: "MapFeature"),
.target(name: "SponsorFeature"),
.target(name: "Theme"),
@@ -133,6 +134,9 @@ var package = Package(
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
),
+ .target(
+ name: "Event"
+ ),
.target(
name: "MapFeature",
dependencies: [
@@ -156,6 +160,7 @@ var package = Package(
name: "SearchFeature",
dependencies: [
.target(name: "CommonComponents"),
+ .target(name: "Event"),
.target(name: "SessionFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
]
@@ -166,6 +171,7 @@ var package = Package(
.target(name: "appioscombined"),
.target(name: "Assets"),
.target(name: "CommonComponents"),
+ .target(name: "Event"),
.target(name: "Model"),
.target(name: "Theme"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
diff --git a/app-ios/Sources/AppFeature/AppView.swift b/app-ios/Sources/AppFeature/AppView.swift
index 4a48153c0..00789cdcf 100644
--- a/app-ios/Sources/AppFeature/AppView.swift
+++ b/app-ios/Sources/AppFeature/AppView.swift
@@ -5,6 +5,7 @@ import Assets
import Auth
import ComposableArchitecture
import Container
+import Event
import MapFeature
import SearchFeature
import SessionFeature
@@ -65,19 +66,22 @@ public struct AppEnvironment {
public let sessionsRepository: SessionsRepository
public let announcementsRepository: AnnouncementsRepository
public let staffRepository: StaffRepository
+ public let eventKitClient: EventKitClientProtocol
public init(
contributorsRepository: ContributorsRepository,
sponsorsRepository: SponsorsRepository,
sessionsRepository: SessionsRepository,
announcementsRepository: AnnouncementsRepository,
- staffRepository: StaffRepository
+ staffRepository: StaffRepository,
+ eventKitClient: EventKitClientProtocol
) {
self.contributorsRepository = contributorsRepository
self.sponsorsRepository = sponsorsRepository
self.sessionsRepository = sessionsRepository
self.announcementsRepository = announcementsRepository
self.staffRepository = staffRepository
+ self.eventKitClient = eventKitClient
}
}
@@ -90,7 +94,8 @@ public extension AppEnvironment {
sponsorsRepository: container.get(type: SponsorsRepository.self),
sessionsRepository: container.get(type: SessionsRepository.self),
announcementsRepository: container.get(type: AnnouncementsRepository.self),
- staffRepository: container.get(type: StaffRepository.self)
+ staffRepository: container.get(type: StaffRepository.self),
+ eventKitClient: EventKitClient()
)
}
@@ -100,7 +105,8 @@ public extension AppEnvironment {
sponsorsRepository: FakeSponsorsRepository(),
sessionsRepository: FakeSessionsRepository(),
announcementsRepository: FakeAnnouncementsRepository(),
- staffRepository: FakeStaffRepository()
+ staffRepository: FakeStaffRepository(),
+ eventKitClient: EventKitClientMock()
)
}
}
@@ -147,7 +153,8 @@ public let appReducer = Reducer.combine(
action: /AppAction.search,
environment: {
.init(
- sessionsRepository: $0.sessionsRepository
+ sessionsRepository: $0.sessionsRepository,
+ eventKitClient: $0.eventKitClient
)
}
),
@@ -155,7 +162,10 @@ public let appReducer = Reducer.combine(
state: \.sessionState,
action: /AppAction.session,
environment: {
- .init(sessionsRepository: $0.sessionsRepository)
+ .init(
+ sessionsRepository: $0.sessionsRepository,
+ eventKitClient: $0.eventKitClient
+ )
}
),
.init { state, action, _ in
diff --git a/app-ios/Sources/Event/Event.swift b/app-ios/Sources/Event/Event.swift
new file mode 100644
index 000000000..197ec75ac
--- /dev/null
+++ b/app-ios/Sources/Event/Event.swift
@@ -0,0 +1,48 @@
+import EventKit
+import UIKit
+
+public protocol EventKitClientProtocol {
+ func requestAccessIfNeeded() async throws -> Bool
+ func addEvent(
+ title: String,
+ startDate: Date,
+ endDate: Date
+ ) throws
+}
+
+public struct EventKitClient: EventKitClientProtocol {
+ private let eventStore = EKEventStore()
+
+ public init() {}
+
+ public func requestAccessIfNeeded() async throws -> Bool {
+ switch EKEventStore.authorizationStatus(for: .event) {
+ case .denied, .restricted:
+ let _ = await UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
+ case .authorized:
+ return true
+ case .notDetermined:
+ break
+ @unknown default:
+ break
+ }
+ return try await eventStore.requestAccess(to: .event)
+ }
+
+ public func addEvent(
+ title: String,
+ startDate: Date,
+ endDate: Date
+ ) throws {
+ guard let defaultCalendar = eventStore.defaultCalendarForNewEvents else {
+ return
+ }
+ let event = EKEvent(eventStore: eventStore)
+ event.title = title
+ event.startDate = startDate
+ event.endDate = endDate
+ event.calendar = defaultCalendar
+
+ try eventStore.save(event, span: .thisEvent)
+ }
+}
diff --git a/app-ios/Sources/Event/Mock.swift b/app-ios/Sources/Event/Mock.swift
new file mode 100644
index 000000000..5643830b6
--- /dev/null
+++ b/app-ios/Sources/Event/Mock.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public struct EventKitClientMock: EventKitClientProtocol {
+ public init() {}
+
+ public func requestAccessIfNeeded() async throws -> Bool {
+ return true
+ }
+
+ public func addEvent(title: String, startDate: Date, endDate: Date) throws {}
+}
diff --git a/app-ios/Sources/SearchFeature/SearchView.swift b/app-ios/Sources/SearchFeature/SearchView.swift
index ce945dd10..351580162 100644
--- a/app-ios/Sources/SearchFeature/SearchView.swift
+++ b/app-ios/Sources/SearchFeature/SearchView.swift
@@ -1,5 +1,6 @@
import CommonComponents
import ComposableArchitecture
+import Event
import Model
import SessionFeature
import SwiftUI
@@ -34,11 +35,14 @@ public enum SearchAction {
public struct SearchEnvironment {
public let sessionsRepository: SessionsRepository
+ public let eventKitClient: EventKitClientProtocol
public init(
- sessionsRepository: SessionsRepository
+ sessionsRepository: SessionsRepository,
+ eventKitClient: EventKitClientProtocol
) {
self.sessionsRepository = sessionsRepository
+ self.eventKitClient = eventKitClient
}
}
@@ -47,7 +51,10 @@ public let searchReducer = Reducer
state: \.sessionState,
action: /SearchAction.session,
environment: {
- .init(sessionsRepository: $0.sessionsRepository)
+ .init(
+ sessionsRepository: $0.sessionsRepository,
+ eventKitClient: $0.eventKitClient
+ )
}
),
.init { state, action, environment in
@@ -101,7 +108,6 @@ public let searchReducer = Reducer
}
)
-
public struct SearchView: View {
private let store: Store
@@ -188,7 +194,8 @@ struct SearchView_Previews: PreviewProvider {
),
reducer: .empty,
environment: SearchEnvironment(
- sessionsRepository: FakeSessionsRepository()
+ sessionsRepository: FakeSessionsRepository(),
+ eventKitClient: EventKitClientMock()
)
)
)
diff --git a/app-ios/Sources/SessionFeature/SessionView.swift b/app-ios/Sources/SessionFeature/SessionView.swift
index 901bb61ad..854eecd35 100644
--- a/app-ios/Sources/SessionFeature/SessionView.swift
+++ b/app-ios/Sources/SessionFeature/SessionView.swift
@@ -1,6 +1,7 @@
import appioscombined
import Assets
import ComposableArchitecture
+import Event
import Model
import SwiftUI
import Theme
@@ -8,6 +9,7 @@ import Theme
public struct SessionState: Equatable {
public var timetableItemWithFavorite: TimetableItemWithFavorite
public var isShareSheetShown: Bool = false
+ public var eventAddConfirmAlert: AlertState?
public init(timetableItemWithFavorite: TimetableItemWithFavorite) {
self.timetableItemWithFavorite = timetableItemWithFavorite
@@ -20,13 +22,21 @@ public enum SessionAction {
case tapFavorite
case tapShare
case hideShareSheet
+ case showEventAddConfirmAlert
+ case hideEventAddConfirmAlert
+ case addEvent
}
public struct SessionEnvironment {
public let sessionsRepository: SessionsRepository
+ public let eventKitClient: EventKitClientProtocol
- public init(sessionsRepository: SessionsRepository) {
+ public init(
+ sessionsRepository: SessionsRepository,
+ eventKitClient: EventKitClientProtocol
+ ) {
self.sessionsRepository = sessionsRepository
+ self.eventKitClient = eventKitClient
}
}
@@ -34,8 +44,11 @@ public let sessionReducer = Reducerスライド
(同時通訳)
このセッションは事情により中止となりました
+ イベントを追加します。
+ OK
+ キャンセル
設定
diff --git a/core/model/src/commonMain/resources/MR/en/strings.xml b/core/model/src/commonMain/resources/MR/en/strings.xml
index 529611adb..8ac9640fb 100644
--- a/core/model/src/commonMain/resources/MR/en/strings.xml
+++ b/core/model/src/commonMain/resources/MR/en/strings.xml
@@ -48,6 +48,9 @@
SLIDE
(interpretation)
このセッションは事情により中止となりました
+ Add an event
+ OK
+ Cancel
Setting
diff --git a/core/model/src/commonMain/resources/MR/zh/strings.xml b/core/model/src/commonMain/resources/MR/zh/strings.xml
index 3a06c6529..e3094e9cf 100644
--- a/core/model/src/commonMain/resources/MR/zh/strings.xml
+++ b/core/model/src/commonMain/resources/MR/zh/strings.xml
@@ -49,6 +49,9 @@
(interpretation)
このセッションは事情により中止となりました
+ Add an event
+ OK
+ Cancel
设置