From 4c2212a49ebc08c7ef54208b2a6b10cf8cd5175d Mon Sep 17 00:00:00 2001 From: vixer93 Date: Wed, 10 Jul 2024 20:02:31 +0900 Subject: [PATCH 1/3] Add toast modifier --- app-ios/Package.swift | 6 +- .../Modifier/ToastModifier.swift | 71 +++++++++++++++++++ .../Resource/Localizable.xcstrings | 16 +++++ .../TimetableDetailReducer.swift | 17 +++-- .../TimetableDetailView.swift | 10 +-- .../AboutFeatureTests/AboutFeatureTests.swift | 41 ----------- .../TimetableDetailTests.swift | 8 ++- 7 files changed, 115 insertions(+), 54 deletions(-) create mode 100644 app-ios/Sources/CommonComponents/Modifier/ToastModifier.swift diff --git a/app-ios/Package.swift b/app-ios/Package.swift index 999c5132b..61383ad2c 100644 --- a/app-ios/Package.swift +++ b/app-ios/Package.swift @@ -65,8 +65,8 @@ let package = Package( .timetableFeature, .timetableDetailFeature, .tca, - "KMPClient", - .product(name: "LicenseList", package: "LicenseList"), + .kmpClient, + .licenseList, ] ), .testTarget( @@ -114,6 +114,7 @@ let package = Package( dependencies: [ .tca, .theme, + .commonComponents ] ), .testTarget( @@ -228,6 +229,7 @@ extension Target.Dependency { static let firebaseAuth: Target.Dependency = .product(name: "FirebaseAuth", package: "firebase-ios-sdk") static let firebaseRemoteConfig: Target.Dependency = .product(name: "FirebaseRemoteConfig", package: "firebase-ios-sdk") static let tca: Target.Dependency = .product(name: "ComposableArchitecture", package: "swift-composable-architecture") + static let licenseList: Target.Dependency = .product(name: "LicenseList", package: "LicenseList") } /// ref: https://github.com/treastrain/swift-upcomingfeatureflags-cheatsheet?tab=readme-ov-file#short diff --git a/app-ios/Sources/CommonComponents/Modifier/ToastModifier.swift b/app-ios/Sources/CommonComponents/Modifier/ToastModifier.swift new file mode 100644 index 000000000..a180aa268 --- /dev/null +++ b/app-ios/Sources/CommonComponents/Modifier/ToastModifier.swift @@ -0,0 +1,71 @@ +import SwiftUI +import Theme + +public struct ToastState: Equatable { + public let text: String + + public init(text: String) { + self.text = text + } +} + +struct ToastModifier: ViewModifier { + @Binding var item: ToastState? + @State private var task: Task? = nil + private let animationDelay = 1.0 + + func body(content: Content) -> some View { + ZStack { + content + + if item != nil { + ToastView(item: $item) + .zIndex(1) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .task { + try? await Task.sleep(for: .seconds(animationDelay)) + } + .onTapGesture { + item = nil + } + } + } + .onChange(of: item) { + task?.cancel() + if item != nil { + task = Task { + try? await Task.sleep(for: .seconds(4)) + withAnimation(.easeInOut(duration: animationDelay)) { + item = nil + } + } + } + } + .animation(.easeInOut(duration: animationDelay), value: item) + } +} + +struct ToastView: View { + @Binding var item: ToastState? + var body: some View { + ZStack { + Text(item?.text ?? "") + .textStyle(.bodyMedium) + .foregroundColor(AssetColors.Inverse.inverseOnSurface.swiftUIColor) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.vertical, 14) + .background(AssetColors.Inverse.inverseSurface.swiftUIColor) + .padding(.horizontal, 12) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + .padding(.vertical, 16) + } +} + +extension View { + public func toast(_ item: Binding) -> some View { + modifier(ToastModifier(item: item)) + } +} diff --git a/app-ios/Sources/TimetableDetailFeature/Resource/Localizable.xcstrings b/app-ios/Sources/TimetableDetailFeature/Resource/Localizable.xcstrings index 2d3fe1bfd..316c74f39 100644 --- a/app-ios/Sources/TimetableDetailFeature/Resource/Localizable.xcstrings +++ b/app-ios/Sources/TimetableDetailFeature/Resource/Localizable.xcstrings @@ -6,6 +6,22 @@ }, "name" : { + }, + "TimetableDetailAddBookmark" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Added to bookmarks" + } + }, + "ja" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "ブックマークに追加されました" + } + } + } }, "TimeTableDetailApplicants" : { "localizations" : { diff --git a/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift b/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift index cad0583b6..eb848fa6c 100644 --- a/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift +++ b/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift @@ -1,4 +1,5 @@ import ComposableArchitecture +import CommonComponents @Reducer public struct TimetableDetailReducer { @@ -6,18 +7,26 @@ public struct TimetableDetailReducer { @ObservableState public struct State: Equatable { - var title: String + var toast: ToastState? } public enum Action { - case onAppear + case view(View) + case setToast(ToastState?) + + public enum View { + case favoriteButtonTapped + } } public var body: some ReducerOf { Reduce { state, action in switch action { - case .onAppear: - state.title = "Timetable Detail" + case .view(.favoriteButtonTapped): + state.toast = .init(text: String(localized: "TimetableDetailAddBookmark", bundle: .module)) + return .none + case let .setToast(toast): + state.toast = toast return .none } } diff --git a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift index 415e2bb4f..ca7465440 100644 --- a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift +++ b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift @@ -1,10 +1,11 @@ import SwiftUI import ComposableArchitecture import Theme +import CommonComponents public struct TimetableDetailView: View { - private let store: StoreOf - + @Bindable private var store: StoreOf + public var body: some View { GeometryReader { proxy in VStack(spacing: 0) { @@ -22,6 +23,7 @@ public struct TimetableDetailView: View { archive .padding(16) } + .toast($store.toast.sending(\.setToast)) footer } @@ -51,7 +53,7 @@ public struct TimetableDetailView: View { } Spacer() Button { - // do something + store.send(.view(.favoriteButtonTapped)) } label: { Group { Image(.icFavorite) @@ -207,7 +209,7 @@ public struct TimetableDetailView: View { #Preview { TimetableDetailView( - store: .init(initialState: .init(title: "")) { + store: .init(initialState: .init(toast: nil)) { TimetableDetailReducer() } ) diff --git a/app-ios/Tests/AboutFeatureTests/AboutFeatureTests.swift b/app-ios/Tests/AboutFeatureTests/AboutFeatureTests.swift index f8e7cac79..836de03a3 100644 --- a/app-ios/Tests/AboutFeatureTests/AboutFeatureTests.swift +++ b/app-ios/Tests/AboutFeatureTests/AboutFeatureTests.swift @@ -4,37 +4,6 @@ import ComposableArchitecture final class AboutFeatureTests: XCTestCase { - @MainActor - func testTappedStaffs() async { - let store = TestStore(initialState: AboutReducer.State()) { - AboutReducer() - } - - await store.send(\.view.staffsTapped) { - $0.path[id: 0] = .staffs - } - } - - @MainActor - func testTappedControbuters() async { - let store = TestStore(initialState: AboutReducer.State()) { - AboutReducer() - } - await store.send(\.view.contributersTapped) { - $0.path[id: 0] = .contributers - } - } - - @MainActor - func testTappedSponsors() async { - let store = TestStore(initialState: AboutReducer.State()) { - AboutReducer() - } - await store.send(\.view.sponsorsTapped) { - $0.path[id: 0] = .sponsors - } - } - @MainActor func testTappedCodeOfConduct() async { let store = TestStore(initialState: AboutReducer.State()) { @@ -45,16 +14,6 @@ final class AboutFeatureTests: XCTestCase { } } - @MainActor - func testTappedAcknowledgements() async { - let store = TestStore(initialState: AboutReducer.State()) { - AboutReducer() - } - await store.send(\.view.acknowledgementsTapped) { - $0.path[id: 0] = .acknowledgements - } - } - @MainActor func testTappedPrivacyPolicy() async { let store = TestStore(initialState: AboutReducer.State()) { diff --git a/app-ios/Tests/TimetableDetailFeatureTests/TimetableDetailTests.swift b/app-ios/Tests/TimetableDetailFeatureTests/TimetableDetailTests.swift index a32ca84a7..24e5868af 100644 --- a/app-ios/Tests/TimetableDetailFeatureTests/TimetableDetailTests.swift +++ b/app-ios/Tests/TimetableDetailFeatureTests/TimetableDetailTests.swift @@ -4,11 +4,13 @@ import ComposableArchitecture final class TimetableDetail_iosTests: XCTestCase { @MainActor func testExample() async throws { - let store = TestStore(initialState: TimetableDetailReducer.State(title: "Test")) { + let store = TestStore(initialState: TimetableDetailReducer.State()) { TimetableDetailReducer() } - await store.send(.onAppear) { - $0.title = "Timetable Detail" + + await store.send(.favoriteButtonTapped) { + $0.toast = .init(text: String(localized: "TimetableDetailAddBookmark", bundle: .module)) } + } } From 9943ab32eff81483615dbe2b4a85a44c0d0c72ab Mon Sep 17 00:00:00 2001 From: vixer93 Date: Fri, 12 Jul 2024 00:01:42 +0900 Subject: [PATCH 2/3] Fix for comment --- .../Sources/CommonComponents/Modifier/ToastModifier.swift | 3 --- .../TimetableDetailFeature/TimetableDetailReducer.swift | 8 ++++---- .../TimetableDetailFeature/TimetableDetailView.swift | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app-ios/Sources/CommonComponents/Modifier/ToastModifier.swift b/app-ios/Sources/CommonComponents/Modifier/ToastModifier.swift index a180aa268..f08eebc32 100644 --- a/app-ios/Sources/CommonComponents/Modifier/ToastModifier.swift +++ b/app-ios/Sources/CommonComponents/Modifier/ToastModifier.swift @@ -22,9 +22,6 @@ struct ToastModifier: ViewModifier { ToastView(item: $item) .zIndex(1) .transition(.move(edge: .bottom).combined(with: .opacity)) - .task { - try? await Task.sleep(for: .seconds(animationDelay)) - } .onTapGesture { item = nil } diff --git a/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift b/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift index eb848fa6c..1282515a2 100644 --- a/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift +++ b/app-ios/Sources/TimetableDetailFeature/TimetableDetailReducer.swift @@ -10,9 +10,9 @@ public struct TimetableDetailReducer { var toast: ToastState? } - public enum Action { + public enum Action: BindableAction { + case binding(BindingAction) case view(View) - case setToast(ToastState?) public enum View { case favoriteButtonTapped @@ -20,13 +20,13 @@ public struct TimetableDetailReducer { } public var body: some ReducerOf { + BindingReducer() Reduce { state, action in switch action { case .view(.favoriteButtonTapped): state.toast = .init(text: String(localized: "TimetableDetailAddBookmark", bundle: .module)) return .none - case let .setToast(toast): - state.toast = toast + case .binding: return .none } } diff --git a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift index ca7465440..5b1a264ec 100644 --- a/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift +++ b/app-ios/Sources/TimetableDetailFeature/TimetableDetailView.swift @@ -23,7 +23,7 @@ public struct TimetableDetailView: View { archive .padding(16) } - .toast($store.toast.sending(\.setToast)) + .toast($store.toast) footer } From 62fe3e69a204c9e3d9177cf8ca5972babb26328a Mon Sep 17 00:00:00 2001 From: vixer93 Date: Fri, 12 Jul 2024 00:13:41 +0900 Subject: [PATCH 3/3] Add theme dependency to common component --- app-ios/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-ios/Package.swift b/app-ios/Package.swift index 61383ad2c..ea5fadb3e 100644 --- a/app-ios/Package.swift +++ b/app-ios/Package.swift @@ -194,7 +194,7 @@ let package = Package( .tca ] ), - .target(name: "CommonComponents"), + .target(name: "CommonComponents", dependencies: [.theme]), // Please run ./gradlew app-ios-shared:assembleSharedXCFramework first .binaryTarget(name: "KmpModule", path: "../app-ios-shared/build/XCFrameworks/debug/shared.xcframework"), ]