From a422ae7cfa458b99d0256ba30b19b096503f4d27 Mon Sep 17 00:00:00 2001 From: vixer93 Date: Fri, 12 Jul 2024 01:19:40 +0900 Subject: [PATCH] Added logic to add bookmark --- app-ios/Package.swift | 4 +- app-ios/Sources/App/RootReducer.swift | 4 ++ app-ios/Sources/KMPClient/TestKey.swift | 7 ++- .../ic_favorite.imageset/ic_favorite.svg | 3 - .../Contents.json | 2 +- .../ic_favorite_fill.imageset/favorite.svg | 8 +++ .../Contents.json | 15 +++++ .../ic_favorite_outline.svg | 8 +++ .../TimetableDetailFeature/SampleData.swift | 2 + .../TimetableDetailReducer.swift | 59 +++++++++++++++++-- .../TimetableDetailView.swift | 34 ++++++----- .../TimetableFeature/TimetableListView.swift | 8 ++- .../TimetableFeature/TimetableReducer.swift | 11 +++- .../TimetableDetailTests.swift | 33 ++++++++++- .../compose/ComposeEffectErrorHandler.kt | 3 +- gradle/libs.versions.toml | 2 +- 16 files changed, 168 insertions(+), 35 deletions(-) delete mode 100644 app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite.imageset/ic_favorite.svg rename app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/{ic_favorite.imageset => ic_favorite_fill.imageset}/Contents.json (83%) create mode 100644 app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_fill.imageset/favorite.svg create mode 100644 app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_outline.imageset/Contents.json create mode 100644 app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_outline.imageset/ic_favorite_outline.svg diff --git a/app-ios/Package.swift b/app-ios/Package.swift index ea5fadb3e..e18f98276 100644 --- a/app-ios/Package.swift +++ b/app-ios/Package.swift @@ -114,7 +114,9 @@ let package = Package( dependencies: [ .tca, .theme, - .commonComponents + .commonComponents, + .kmpClient, + .kmpModule, ] ), .testTarget( diff --git a/app-ios/Sources/App/RootReducer.swift b/app-ios/Sources/App/RootReducer.swift index a34d26ee2..6aaa1f4be 100644 --- a/app-ios/Sources/App/RootReducer.swift +++ b/app-ios/Sources/App/RootReducer.swift @@ -100,6 +100,10 @@ public struct RootReducer { case .about(.view(.acknowledgementsTapped)): state.paths.about.append(.acknowledgements) return .none + + case .timetable(.view(.timetableItemTapped)): + state.paths.timetable.append(.timetableDetail(TimetableDetailReducer.State())) + return .none default: return .none diff --git a/app-ios/Sources/KMPClient/TestKey.swift b/app-ios/Sources/KMPClient/TestKey.swift index b38408eb5..074589bca 100644 --- a/app-ios/Sources/KMPClient/TestKey.swift +++ b/app-ios/Sources/KMPClient/TestKey.swift @@ -2,7 +2,12 @@ import Dependencies extension TimetableClient: TestDependencyKey { public static let previewValue: Self = Self() - public static let testValue: Self = Self() + + public static let testValue: Self = Self( + streamTimetable: unimplemented("TimetableClient.streamTimetable"), + streamTimetableItemWithFavorite: unimplemented("TimetableClient.streamTimetableItemWithFavorite"), + toggleBookmark: unimplemented("TimetableClient.toggleBookmark") + ) } extension StaffClient: TestDependencyKey { diff --git a/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite.imageset/ic_favorite.svg b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite.imageset/ic_favorite.svg deleted file mode 100644 index 8542f5563..000000000 --- a/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite.imageset/ic_favorite.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite.imageset/Contents.json b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_fill.imageset/Contents.json similarity index 83% rename from app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite.imageset/Contents.json rename to app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_fill.imageset/Contents.json index 6eb84ed4e..3a36c12e6 100644 --- a/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite.imageset/Contents.json +++ b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_fill.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_favorite.svg", + "filename" : "favorite.svg", "idiom" : "universal" } ], diff --git a/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_fill.imageset/favorite.svg b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_fill.imageset/favorite.svg new file mode 100644 index 000000000..e3fbd7e6a --- /dev/null +++ b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_fill.imageset/favorite.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_outline.imageset/Contents.json b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_outline.imageset/Contents.json new file mode 100644 index 000000000..0a1fa3779 --- /dev/null +++ b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_outline.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ic_favorite_outline.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_outline.imageset/ic_favorite_outline.svg b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_outline.imageset/ic_favorite_outline.svg new file mode 100644 index 000000000..c4eefef24 --- /dev/null +++ b/app-ios/Sources/TimetableDetailFeature/Resource/Media.xcassets/ic_favorite_outline.imageset/ic_favorite_outline.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app-ios/Sources/TimetableDetailFeature/SampleData.swift b/app-ios/Sources/TimetableDetailFeature/SampleData.swift index 46e5d8eef..e55dda424 100644 --- a/app-ios/Sources/TimetableDetailFeature/SampleData.swift +++ b/app-ios/Sources/TimetableDetailFeature/SampleData.swift @@ -1,3 +1,5 @@ +import shared + enum SampleData { static let title = "DroidKaigiアプリで見るアーキテクチャの変遷" static let dateValue = "2023.09.14 / 11:20 ~ 12:00 (40分)" diff --git a/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift b/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift index 1282515a2..173b671a0 100644 --- a/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift +++ b/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift @@ -1,31 +1,80 @@ import ComposableArchitecture import CommonComponents +import KMPClient +import shared @Reducer -public struct TimetableDetailReducer { +public struct TimetableDetailReducer: Sendable { + @Dependency(\.timetableClient) private var timetableClient + public init() {} @ObservableState public struct State: Equatable { + public init(isBookmarked: Bool = false, toast: ToastState? = nil) { + self.isBookmarked = isBookmarked + self.toast = toast + } + + var isBookmarked = false var toast: ToastState? } - + public enum Action: BindableAction { case binding(BindingAction) case view(View) + case bookmarkResponse(Result) public enum View { - case favoriteButtonTapped + case bookmarkButtonTapped + case shareButtonTapped + case calendarButtonTapped + case slideButtonTapped + case videoButtonTapped } } public var body: some ReducerOf { BindingReducer() Reduce { state, action in + enum CancelID { case request } + switch action { - case .view(.favoriteButtonTapped): - state.toast = .init(text: String(localized: "TimetableDetailAddBookmark", bundle: .module)) + case .view(.bookmarkButtonTapped): + return .run { @Sendable send in + do { + await send(.bookmarkResponse(.success( + try await timetableClient.toggleBookmark(id: TimetableItemId(value: "")) + ))) + } catch { + await send(.bookmarkResponse(.failure(error))) + } + } + .cancellable(id: CancelID.request) + + case .view(.calendarButtonTapped): + return .none + + case .view(.shareButtonTapped): return .none + + case .view(.slideButtonTapped): + return .none + + case .view(.videoButtonTapped): + return .none + + case .bookmarkResponse(.success): + if !state.isBookmarked { + state.toast = .init(text: String(localized: "TimetableDetailAddBookmark", bundle: .module)) + } + state.isBookmarked.toggle() + return .none + + case let .bookmarkResponse(.failure(error)): + print(error) + return .none + case .binding: return .none } diff --git a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift index 5b1a264ec..c2c8e3a4d 100644 --- a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift +++ b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift @@ -31,12 +31,13 @@ public struct TimetableDetailView: View { .frame(maxWidth: .infinity) .ignoresSafeArea(edges: [.top]) } + .toolbarBackground(AssetColors.Surface.surface.swiftUIColor, for: .navigationBar) } - @ViewBuilder var footer: some View { + @MainActor var footer: some View { HStack(spacing: 8) { Button { - // do something + store.send(.view(.shareButtonTapped)) } label: { Group { Image(.icShare) @@ -44,7 +45,7 @@ public struct TimetableDetailView: View { .frame(width: 40, height: 40) } Button { - // do something + store.send(.view(.calendarButtonTapped)) } label: { Group { Image(.icAddCalendar) @@ -53,13 +54,18 @@ public struct TimetableDetailView: View { } Spacer() Button { - store.send(.view(.favoriteButtonTapped)) + store.send(.view(.bookmarkButtonTapped)) } label: { Group { - Image(.icFavorite) + if store.isBookmarked { + Image(.icFavoriteFill) + } else { + Image(.icFavoriteOutline) + } + } .frame(width: 56, height: 56) - .background(AssetColors.Surface.surfaceContainer.swiftUIColor) + .background(AssetColors.Secondary.secondaryContainer.swiftUIColor) .clipShape(RoundedRectangle(cornerRadius: 16)) } } @@ -69,7 +75,7 @@ public struct TimetableDetailView: View { .background(AssetColors.Surface.surfaceContainer.swiftUIColor) } - @ViewBuilder var headLine: some View { + @MainActor var headLine: some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 4) { RoomTag(.arcticFox) @@ -99,11 +105,11 @@ public struct TimetableDetailView: View { } .padding(.bottom, 20) } - .padding(.horizontal, 16) + .padding([.top, .horizontal], 16) .background(AssetColors.Custom.arcticFoxContainer.swiftUIColor) } - @ViewBuilder var detail: some View { + @MainActor var detail: some View { VStack(alignment: .leading, spacing: 20) { VStack(spacing: 16) { InformationRow( @@ -143,7 +149,7 @@ public struct TimetableDetailView: View { } } - @ViewBuilder var applicants: some View { + @MainActor var applicants: some View { VStack(alignment: .leading, spacing: 16) { Text(String(localized: "TimeTableDetailApplicants", bundle: .module)) .textStyle(.titleLarge) @@ -155,7 +161,7 @@ public struct TimetableDetailView: View { } } - @ViewBuilder var archive: some View { + @MainActor var archive: some View { VStack(alignment: .leading, spacing: 16) { Text(String(localized: "TimeTableDetailArchive", bundle: .module)) .textStyle(.titleLarge) @@ -163,7 +169,7 @@ public struct TimetableDetailView: View { HStack { Button { - // do something + store.send(.view(.slideButtonTapped)) } label: { VStack { Label( @@ -181,7 +187,7 @@ public struct TimetableDetailView: View { .clipShape(Capsule()) } Button { - // do something + store.send(.view(.videoButtonTapped)) } label: { VStack { Label( @@ -209,7 +215,7 @@ public struct TimetableDetailView: View { #Preview { TimetableDetailView( - store: .init(initialState: .init(toast: nil)) { + store: .init(initialState: .init()) { TimetableDetailReducer() } ) diff --git a/app-ios/Sources/TimetableFeature/TimetableListView.swift b/app-ios/Sources/TimetableFeature/TimetableListView.swift index ba9c493f7..576c78d82 100644 --- a/app-ios/Sources/TimetableFeature/TimetableListView.swift +++ b/app-ios/Sources/TimetableFeature/TimetableListView.swift @@ -13,7 +13,7 @@ public struct TimetableView: View { HStack { ForEach(DayTab.allCases) { tabItem in Button(action: { - store.send(.selectDay(tabItem)) + store.send(.view(.selectDay(tabItem))) }, label: { //TODO: Only selected button should be green and underlined Text(tabItem.rawValue).foregroundStyle(Color(.greenSelectColorset)) @@ -41,7 +41,11 @@ struct TimetableListView: View { ScrollView{ LazyVStack { ForEach(store.timetableItems, id: \.self) { item in - TimeGroupMiniList(contents: item) + Button { + store.send(.view(.timetableItemTapped)) + } label: { + TimeGroupMiniList(contents: item) + } } }.scrollContentBackground(.hidden) diff --git a/app-ios/Sources/TimetableFeature/TimetableReducer.swift b/app-ios/Sources/TimetableFeature/TimetableReducer.swift index f040c8313..ed5bc6b38 100644 --- a/app-ios/Sources/TimetableFeature/TimetableReducer.swift +++ b/app-ios/Sources/TimetableFeature/TimetableReducer.swift @@ -17,8 +17,13 @@ public struct TimetableReducer { } public enum Action { + case view(View) case onAppear - case selectDay(DayTab) + + public enum View { + case selectDay(DayTab) + case timetableItemTapped + } } public var body: some Reducer { @@ -27,7 +32,9 @@ public struct TimetableReducer { case .onAppear: state.timetableItems = sampleData.day1Results return .none - case .selectDay(let dayTab): + case .view(.timetableItemTapped): + return .none + case .view(.selectDay(let dayTab)): //TODO: Replace with real data switch dayTab { diff --git a/app-ios/Tests/TimetableDetailFeatureTests/TimetableDetailTests.swift b/app-ios/Tests/TimetableDetailFeatureTests/TimetableDetailTests.swift index 24e5868af..f26a31a83 100644 --- a/app-ios/Tests/TimetableDetailFeatureTests/TimetableDetailTests.swift +++ b/app-ios/Tests/TimetableDetailFeatureTests/TimetableDetailTests.swift @@ -3,14 +3,41 @@ import ComposableArchitecture @testable import TimetableDetailFeature final class TimetableDetail_iosTests: XCTestCase { - @MainActor func testExample() async throws { - let store = TestStore(initialState: TimetableDetailReducer.State()) { + @MainActor func testTappedBookmarkButton() async throws { + let isBookmarked = false + let store = TestStore(initialState: TimetableDetailReducer.State(isBookmarked: isBookmarked)) { TimetableDetailReducer() + } withDependencies: { + $0.timetableClient.toggleBookmark = { @Sendable _ in } } - await store.send(.favoriteButtonTapped) { + // add bookmark + await store.send(.view(.bookmarkButtonTapped)) + await store.receive(\.bookmarkResponse) { + $0.isBookmarked = !isBookmarked $0.toast = .init(text: String(localized: "TimetableDetailAddBookmark", bundle: .module)) } + // remove bookmark + await store.send(.view(.bookmarkButtonTapped)) + await store.receive(\.bookmarkResponse) { + $0.isBookmarked = isBookmarked + } + + } + + @MainActor func testTappedBookmarkButtonFailed() async throws { + let store = TestStore(initialState: TimetableDetailReducer.State(isBookmarked: false)) { + TimetableDetailReducer() + } withDependencies: { + $0.timetableClient.toggleBookmark = { @Sendable _ in throw TimetableDetailTestError.fail } + } + + await store.send(.view(.bookmarkButtonTapped)) + await store.receive(\.bookmarkResponse) } } + +enum TimetableDetailTestError: Error { + case fail +} diff --git a/core/common/src/commonMain/kotlin/io/github/droidkaigi/confsched/compose/ComposeEffectErrorHandler.kt b/core/common/src/commonMain/kotlin/io/github/droidkaigi/confsched/compose/ComposeEffectErrorHandler.kt index e4dfe2a55..acd388039 100644 --- a/core/common/src/commonMain/kotlin/io/github/droidkaigi/confsched/compose/ComposeEffectErrorHandler.kt +++ b/core/common/src/commonMain/kotlin/io/github/droidkaigi/confsched/compose/ComposeEffectErrorHandler.kt @@ -1,6 +1,5 @@ package io.github.droidkaigi.confsched.compose -import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.LaunchedEffect @@ -78,7 +77,7 @@ fun Flow.safeCollectAsRetainedState( } } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun StateFlow.safeCollectAsRetainedState( context: CoroutineContext = EmptyCoroutineContext, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 862b71e32..bafc87150 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -androidGradlePlugin = "8.1.4" +androidGradlePlugin = "8.2.2" # For updating Kotlin and Compose Compiler version, see: # https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html#jetpack-compose-and-compose-multiplatform-release-cycles # https://developer.android.com/jetpack/androidx/releases/compose-kotlin?#pre-release_kotlin_compatibility