From 9056130d3bb36defc82151ab85fda92bf4786392 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Mon, 18 Mar 2024 19:00:37 +0900 Subject: [PATCH 01/13] add favorited only filter item on topBarLeading and toggle state when tapped --- .../Sources/ScheduleFeature/Schedule.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index 543829b..71c6a6f 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -26,6 +26,7 @@ public struct Schedule { var day1: Conference? var day2: Conference? var workshop: Conference? + var favoritedOnlyFilterEnabled: Bool = false @Presents var destination: Destination.State? public init() { @@ -44,6 +45,7 @@ public struct Schedule { case disclosureTapped(Session) case mapItemTapped case favoriteIconTapped(Session) + case favoritedOnlyFilterItemTapped } } @@ -111,6 +113,9 @@ public struct Schedule { try? dataClient.saveDay2(day2) try? dataClient.saveWorkshop(workshop) } + case .view(.favoritedOnlyFilterItemTapped): + state.favoritedOnlyFilterEnabled.toggle() + return .none case .binding, .path, .destination: return .none } @@ -195,6 +200,13 @@ public struct ScheduleView: View { .popoverTip(mapTip) } + + ToolbarItem(placement: .topBarLeading) { + favoritedOnlyFilter(enabled: store.favoritedOnlyFilterEnabled) + .onTapGesture { + send(.favoritedOnlyFilterItemTapped) + } + } } .onAppear(perform: { send(.onAppear) @@ -203,6 +215,17 @@ public struct ScheduleView: View { .searchable(text: $store.searchText, isPresented: $store.isSearchBarPresented) } + @ViewBuilder + func favoritedOnlyFilter(enabled: Bool) -> some View { + if enabled { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + } else { + Image(systemName: "star") + .foregroundColor(.gray) + } + } + @ViewBuilder func conferenceList(conference: Conference) -> some View { VStack(alignment: .leading, spacing: 8) { From ac8c2bd81ac255536289fdef20b05886e560ae47 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Mon, 18 Mar 2024 19:30:48 +0900 Subject: [PATCH 02/13] use favoritedOnlyFilterEnabled just to show all items or nothing --- .../ScheduleFeature/Localizable.xcstrings | 10 +++ .../Sources/ScheduleFeature/Schedule.swift | 70 +++++++++++++------ 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings b/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings index edceb1e..b09d0e2 100644 --- a/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings +++ b/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings @@ -759,6 +759,16 @@ } } }, + "No items to show." : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示できるものがありません。" + } + } + } + }, "Office hour %@" : { "extractionState" : "manual", "localizations" : { diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index 71c6a6f..2fa707a 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -231,29 +231,36 @@ public struct ScheduleView: View { VStack(alignment: .leading, spacing: 8) { Text(conference.date, style: .date) .font(.title2) - ForEach(conference.schedules, id: \.self) { schedule in - VStack(alignment: .leading, spacing: 4) { - Text(schedule.time, style: .time) - .font(.subheadline.bold()) - ForEach(schedule.sessions, id: \.self) { session in - if session.description != nil { - Button { - send(.disclosureTapped(session)) - } label: { - listRow(session: session) - .padding() + + if hasNoItemsToShow(on: conference) { + noItemsToShowMessage() + } else { + ForEach(conference.schedules, id: \.self) { schedule in + VStack(alignment: .leading, spacing: 4) { + if hasSomeItemsToShow(on: schedule) { + Text(schedule.time, style: .time) + .font(.subheadline.bold()) + ForEach(schedule.sessions, id: \.self) { session in + if session.description != nil { + Button { + send(.disclosureTapped(session)) + } label: { + listRow(session: session) + .padding() + } + .background( + Color(uiColor: .secondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: 8)) + ) + } else { + listRow(session: session) + .padding() + .background( + Color(uiColor: .secondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: 8)) + ) + } } - .background( - Color(uiColor: .secondarySystemBackground) - .clipShape(RoundedRectangle(cornerRadius: 8)) - ) - } else { - listRow(session: session) - .padding() - .background( - Color(uiColor: .secondarySystemBackground) - .clipShape(RoundedRectangle(cornerRadius: 8)) - ) } } } @@ -262,6 +269,25 @@ public struct ScheduleView: View { .padding() } + func hasSomeItemsToShow(on schedule: SharedModels.Schedule) -> Bool { + store.favoritedOnlyFilterEnabled + } + + func hasNoItemsToShow(on conference: Conference) -> Bool { + !store.favoritedOnlyFilterEnabled + } + + @ViewBuilder + func noItemsToShowMessage() -> some View { + Text(String(localized: "No items to show.", bundle: .module)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background( + Color(uiColor: .secondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: 8)) + ) + } + @ViewBuilder func listRow(session: Session) -> some View { HStack(spacing: 8) { From 993463f591cd5d5c66283e25d36b336f1027e253 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Mon, 18 Mar 2024 21:29:02 +0900 Subject: [PATCH 03/13] filter sessions according to favorited only filter --- .../Sources/ScheduleFeature/Schedule.swift | 73 +++++++++++-------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index 2fa707a..a2b0175 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -232,51 +232,43 @@ public struct ScheduleView: View { Text(conference.date, style: .date) .font(.title2) - if hasNoItemsToShow(on: conference) { - noItemsToShowMessage() - } else { - ForEach(conference.schedules, id: \.self) { schedule in + let schedules = store.favoritedOnlyFilterEnabled ? + conference.schedules.filteredFavoritedOnly : conference.schedules + if schedules.count > 0 { + ForEach(schedules, id: \.self) { schedule in VStack(alignment: .leading, spacing: 4) { - if hasSomeItemsToShow(on: schedule) { - Text(schedule.time, style: .time) - .font(.subheadline.bold()) - ForEach(schedule.sessions, id: \.self) { session in - if session.description != nil { - Button { - send(.disclosureTapped(session)) - } label: { - listRow(session: session) - .padding() - } + Text(schedule.time, style: .time) + .font(.subheadline.bold()) + ForEach(schedule.sessions, id: \.self) { session in + if session.description != nil { + Button { + send(.disclosureTapped(session)) + } label: { + listRow(session: session) + .padding() + } + .background( + Color(uiColor: .secondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: 8)) + ) + } else { + listRow(session: session) + .padding() .background( Color(uiColor: .secondarySystemBackground) .clipShape(RoundedRectangle(cornerRadius: 8)) ) - } else { - listRow(session: session) - .padding() - .background( - Color(uiColor: .secondarySystemBackground) - .clipShape(RoundedRectangle(cornerRadius: 8)) - ) - } } } } } + } else { + noItemsToShowMessage() } } .padding() } - func hasSomeItemsToShow(on schedule: SharedModels.Schedule) -> Bool { - store.favoritedOnlyFilterEnabled - } - - func hasNoItemsToShow(on conference: Conference) -> Bool { - !store.favoritedOnlyFilterEnabled - } - @ViewBuilder func noItemsToShowMessage() -> some View { Text(String(localized: "No items to show.", bundle: .module)) @@ -387,6 +379,25 @@ struct MapTip: Tip, Equatable { var image: Image? = .init(systemName: "map.circle.fill") } +private extension [Session] { + var favorited: Self { + return self.filter { + guard let isFavorited = $0.isFavorited else { + return false + } + return isFavorited + } + } +} + +private extension [SharedModels.Schedule] { + var filteredFavoritedOnly: Self { + self + .map { SharedModels.Schedule(time: $0.time, sessions: $0.sessions.favorited) } + .filter { $0.sessions.count > 0 } + } +} + #Preview { ScheduleView( store: .init( From 7c363b8956a86808a23035d6400cb366f6d586fe Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Tue, 19 Mar 2024 09:16:17 +0900 Subject: [PATCH 04/13] change favorited only filter style to show pulldown picker --- .../ScheduleFeature/Localizable.xcstrings | 30 ++++++++++++ .../Sources/ScheduleFeature/Schedule.swift | 48 +++++++++++-------- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings b/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings index b09d0e2..d1b018f 100644 --- a/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings +++ b/MyLibrary/Sources/ScheduleFeature/Localizable.xcstrings @@ -55,6 +55,16 @@ } } }, + "All" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "全て" + } + } + } + }, "As most people tend to use cloud sync services to store relevant content, we will explore File Provider framework on both iOS and macOS and all the related features: Finder and Files app integration, remote synchronisation with upload and downloads. So let’s explore how it works on both iOS and macOS and how you can sync, upload and download files on these platforms." : { "extractionState" : "manual", "localizations" : { @@ -326,6 +336,26 @@ } } }, + "Favorite" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "お気に入り" + } + } + } + }, + "Filter" : { + "localizations" : { + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フィルター" + } + } + } + }, "Getting started with controlling LEGO using Swift" : { "extractionState" : "manual", "localizations" : { diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index a2b0175..de00b58 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -27,6 +27,7 @@ public struct Schedule { var day2: Conference? var workshop: Conference? var favoritedOnlyFilterEnabled: Bool = false + var selectedFilter: Action.FilterItem = .all @Presents var destination: Destination.State? public init() { @@ -45,7 +46,11 @@ public struct Schedule { case disclosureTapped(Session) case mapItemTapped case favoriteIconTapped(Session) - case favoritedOnlyFilterItemTapped + } + + public enum FilterItem: String, CaseIterable { + case all = "All" + case favorite = "Favorite" } } @@ -113,9 +118,6 @@ public struct Schedule { try? dataClient.saveDay2(day2) try? dataClient.saveWorkshop(workshop) } - case .view(.favoritedOnlyFilterItemTapped): - state.favoritedOnlyFilterEnabled.toggle() - return .none case .binding, .path, .destination: return .none } @@ -202,10 +204,19 @@ public struct ScheduleView: View { } ToolbarItem(placement: .topBarLeading) { - favoritedOnlyFilter(enabled: store.favoritedOnlyFilterEnabled) - .onTapGesture { - send(.favoritedOnlyFilterItemTapped) + Menu { + Picker(String(localized: "Filter", bundle: .module), selection: $store.selectedFilter, content: { + ForEach(Schedule.Action.FilterItem.allCases, id:\.self) { item in + Text(String(localized: String.LocalizationValue(item.rawValue), bundle: .module)) + .tag(item) + } + }) + } label: { + HStack { + Image(systemName: "line.horizontal.3.decrease") + Text(String(localized: "Filter", bundle: .module)) } + } } } .onAppear(perform: { @@ -215,25 +226,13 @@ public struct ScheduleView: View { .searchable(text: $store.searchText, isPresented: $store.isSearchBarPresented) } - @ViewBuilder - func favoritedOnlyFilter(enabled: Bool) -> some View { - if enabled { - Image(systemName: "star.fill") - .foregroundColor(.yellow) - } else { - Image(systemName: "star") - .foregroundColor(.gray) - } - } - @ViewBuilder func conferenceList(conference: Conference) -> some View { VStack(alignment: .leading, spacing: 8) { Text(conference.date, style: .date) .font(.title2) - let schedules = store.favoritedOnlyFilterEnabled ? - conference.schedules.filteredFavoritedOnly : conference.schedules + let schedules = extractFilteredSchedules(from: conference) if schedules.count > 0 { ForEach(schedules, id: \.self) { schedule in VStack(alignment: .leading, spacing: 4) { @@ -350,6 +349,15 @@ public struct ScheduleView: View { } } + func extractFilteredSchedules(from conference: Conference) -> [SharedModels.Schedule] { + switch store.selectedFilter { + case .all: + return conference.schedules + case .favorite: + return conference.schedules.filteredFavoritedOnly + } + } + func officeHourTitle(speakers: [Speaker]) -> String { let names = givenNameList(speakers: speakers) return String(localized: "Office hour \(names)", bundle: .module) From 9d12f9817d965a269710868af5956110c9f2acdd Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Tue, 19 Mar 2024 09:18:48 +0900 Subject: [PATCH 05/13] swap filter item position with map item for putting filter item next to search bar on iPad --- .../Sources/ScheduleFeature/Schedule.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index de00b58..f9b2241 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -195,15 +195,6 @@ public struct ScheduleView: View { } .toolbar { ToolbarItem(placement: .topBarTrailing) { - Image(systemName: "map") - .onTapGesture { - send(.mapItemTapped) - } - .popoverTip(mapTip) - - } - - ToolbarItem(placement: .topBarLeading) { Menu { Picker(String(localized: "Filter", bundle: .module), selection: $store.selectedFilter, content: { ForEach(Schedule.Action.FilterItem.allCases, id:\.self) { item in @@ -213,11 +204,20 @@ public struct ScheduleView: View { }) } label: { HStack { - Image(systemName: "line.horizontal.3.decrease") Text(String(localized: "Filter", bundle: .module)) + Image(systemName: "line.horizontal.3.decrease") } } } + + ToolbarItem(placement: .topBarLeading) { + Image(systemName: "map") + .onTapGesture { + send(.mapItemTapped) + } + .popoverTip(mapTip) + + } } .onAppear(perform: { send(.onAppear) From 24ad60beb78f9a9466e7eb4ace797e2db5cbe355 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Wed, 20 Mar 2024 00:00:25 +0900 Subject: [PATCH 06/13] remove unused property --- MyLibrary/Sources/ScheduleFeature/Schedule.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index f9b2241..f86878d 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -26,7 +26,6 @@ public struct Schedule { var day1: Conference? var day2: Conference? var workshop: Conference? - var favoritedOnlyFilterEnabled: Bool = false var selectedFilter: Action.FilterItem = .all @Presents var destination: Destination.State? From 4d2590a83606d107d28fd61562df5f17b8a5a531 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:22:55 +0900 Subject: [PATCH 07/13] move [Schedule].filtered to SharedModels and add a test --- App/App/App.xctestplan | 7 +++++++ MyLibrary/Package.swift | 7 +++++++ .../Sources/ScheduleFeature/Schedule.swift | 8 -------- .../SharedModels/Conference+Extension.swift | 7 +++++++ .../SharedModelsTests/ConferenceTests.swift | 18 ++++++++++++++++++ MyLibrary/Tests/SharedModelsTests/Mocks.swift | 6 ++++++ 6 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 MyLibrary/Sources/SharedModels/Conference+Extension.swift create mode 100644 MyLibrary/Tests/SharedModelsTests/ConferenceTests.swift create mode 100644 MyLibrary/Tests/SharedModelsTests/Mocks.swift diff --git a/App/App/App.xctestplan b/App/App/App.xctestplan index 3db89c3..89c3ac0 100644 --- a/App/App/App.xctestplan +++ b/App/App/App.xctestplan @@ -23,6 +23,13 @@ "identifier" : "ScheduleFeatureTests", "name" : "ScheduleFeatureTests" } + }, + { + "target" : { + "containerPath" : "container:..\/MyLibrary", + "identifier" : "SharedModelsTests", + "name" : "SharedModelsTests" + } } ], "version" : 1 diff --git a/MyLibrary/Package.swift b/MyLibrary/Package.swift index a97a05e..64f0f4a 100644 --- a/MyLibrary/Package.swift +++ b/MyLibrary/Package.swift @@ -95,5 +95,12 @@ let package = Package( .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] ), + .testTarget( + name: "SharedModelsTests", + dependencies: [ + "SharedModels", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ] + ), ] ) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index afffeba..f90373a 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -391,14 +391,6 @@ public struct ScheduleView: View { } } -private extension [SharedModels.Schedule] { - func filtered(using favorites: Favorites, in day: Conference) -> Self { - self - .map { SharedModels.Schedule(time: $0.time, sessions: $0.sessions.filter { favorites.isFavorited($0, in: day) }) } - .filter { $0.sessions.count > 0 } - } -} - #Preview { ScheduleView( store: .init( diff --git a/MyLibrary/Sources/SharedModels/Conference+Extension.swift b/MyLibrary/Sources/SharedModels/Conference+Extension.swift new file mode 100644 index 0000000..9b89ed6 --- /dev/null +++ b/MyLibrary/Sources/SharedModels/Conference+Extension.swift @@ -0,0 +1,7 @@ +extension [Schedule] { + public func filtered(using favorites: Favorites, in day: Conference) -> Self { + self + .map { Schedule(time: $0.time, sessions: $0.sessions.filter { favorites.isFavorited($0, in: day) }) } + .filter { $0.sessions.count > 0 } + } +} diff --git a/MyLibrary/Tests/SharedModelsTests/ConferenceTests.swift b/MyLibrary/Tests/SharedModelsTests/ConferenceTests.swift new file mode 100644 index 0000000..b254c08 --- /dev/null +++ b/MyLibrary/Tests/SharedModelsTests/ConferenceTests.swift @@ -0,0 +1,18 @@ +import ComposableArchitecture +import XCTest + +@testable import SharedModels + +final class ConferenceTests: XCTestCase { + @MainActor + func testSchedulesFiltered() { + let schedulesWith2Sessions = [Schedule(time: Date(timeIntervalSince1970: 10_000), sessions: [.mock1, .mock2])] + let conference = Conference(id: 1, title: "conference", date: Date(timeIntervalSince1970: 1_000), schedules: schedulesWith2Sessions) + let favoritesMock1Only = Favorites(eachConferenceFavorites: [(conference, [.mock1])]) + + let actual = schedulesWith2Sessions.filtered(using: favoritesMock1Only, in: conference) + + let schedulesMock1Only = [Schedule(time: Date(timeIntervalSince1970: 10_000), sessions: [.mock1])] + XCTAssertEqual(actual, schedulesMock1Only) + } +} diff --git a/MyLibrary/Tests/SharedModelsTests/Mocks.swift b/MyLibrary/Tests/SharedModelsTests/Mocks.swift new file mode 100644 index 0000000..4af3d3f --- /dev/null +++ b/MyLibrary/Tests/SharedModelsTests/Mocks.swift @@ -0,0 +1,6 @@ +import SharedModels + +extension Session { + static let mock1 = Session(title: "1", speakers: nil, place: nil, description: nil, requirements: nil) + static let mock2 = Session(title: "2", speakers: nil, place: nil, description: nil, requirements: nil) +} From 1772e947917fca7b7e47386977d730bce8fe6bd8 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:44:58 +0900 Subject: [PATCH 08/13] separate FilterItem from Action since selectedFilter is binded with Picker and FilterItem doesn't need to be Action --- MyLibrary/Sources/ScheduleFeature/Schedule.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index f90373a..dd0aea2 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -32,7 +32,7 @@ public struct Schedule { var day1: Conference? var day2: Conference? var workshop: Conference? - var selectedFilter: Action.FilterItem = .all + var selectedFilter: FilterItem = .all var favorites: Favorites = .init(eachConferenceFavorites: []) @Presents var destination: Destination.State? @@ -52,11 +52,11 @@ public struct Schedule { case disclosureTapped(Session) case favoriteIconTapped(Session) } + } - public enum FilterItem: String, CaseIterable { - case all = "All" - case favorite = "Favorite" - } + public enum FilterItem: String, CaseIterable { + case all = "All" + case favorite = "Favorite" } @Reducer(state: .equatable) @@ -198,7 +198,7 @@ public struct ScheduleView: View { ToolbarItem(placement: .topBarTrailing) { Menu { Picker(String(localized: "Filter", bundle: .module), selection: $store.selectedFilter, content: { - ForEach(Schedule.Action.FilterItem.allCases, id:\.self) { item in + ForEach(Schedule.FilterItem.allCases, id:\.self) { item in Text(String(localized: String.LocalizationValue(item.rawValue), bundle: .module)) .tag(item) } From 4fe7be7312fdfe02010db59c3be0c804ebaf869e Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:49:52 +0900 Subject: [PATCH 09/13] change FilterItem to inherit LocalizedStringKey for more localization safety --- MyLibrary/Sources/ScheduleFeature/Schedule.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index dd0aea2..ace02fb 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -54,7 +54,7 @@ public struct Schedule { } } - public enum FilterItem: String, CaseIterable { + public enum FilterItem: LocalizedStringKey, CaseIterable { case all = "All" case favorite = "Favorite" } @@ -199,13 +199,13 @@ public struct ScheduleView: View { Menu { Picker(String(localized: "Filter", bundle: .module), selection: $store.selectedFilter, content: { ForEach(Schedule.FilterItem.allCases, id:\.self) { item in - Text(String(localized: String.LocalizationValue(item.rawValue), bundle: .module)) + Text(item.rawValue, bundle: .module) .tag(item) } }) } label: { HStack { - Text(String(localized: "Filter", bundle: .module)) + Text("Filter", bundle: .module) Image(systemName: "line.horizontal.3.decrease") } } @@ -262,7 +262,7 @@ public struct ScheduleView: View { @ViewBuilder func noItemsToShowMessage() -> some View { - Text(String(localized: "No items to show.", bundle: .module)) + Text("No items to show.", bundle: .module) .frame(maxWidth: .infinity, alignment: .leading) .padding() .background( From 4c6adebaa3b3ce7b2aeceab0c1f6bcfe807e9011 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Sun, 31 Mar 2024 23:39:32 +0900 Subject: [PATCH 10/13] change variable name of Conference, day to conference, as other place does --- MyLibrary/Sources/ScheduleFeature/Schedule.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index 82677f7..4291fcb 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -427,11 +427,11 @@ public struct ScheduleView: View { } private extension [SharedModels.Schedule] { - func filtered(using favorites: Favorites, in day: Conference) -> Self { + func filtered(using favorites: Favorites, in conference: Conference) -> Self { self .map { SharedModels.Schedule(time: $0.time, sessions: $0.sessions.filter { - guard let favorites = favorites[day.title] else { + guard let favorites = favorites[conference.title] else { return false } return favorites.contains($0) From 6c7a050429ce5e90529d86388bc9f39d2da0c9d9 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Mon, 1 Apr 2024 00:07:46 +0900 Subject: [PATCH 11/13] because deleted Conference+Extension, move testSchedulesFiltered to ScheduleTests --- App/App/App.xctestplan | 7 ------- MyLibrary/Package.swift | 7 ------- .../Sources/ScheduleFeature/Schedule.swift | 2 +- .../ScheduleFeatureTests/ScheduleTests.swift | 12 ++++++++++++ .../SharedModelsTests/ConferenceTests.swift | 18 ------------------ MyLibrary/Tests/SharedModelsTests/Mocks.swift | 6 ------ 6 files changed, 13 insertions(+), 39 deletions(-) delete mode 100644 MyLibrary/Tests/SharedModelsTests/ConferenceTests.swift delete mode 100644 MyLibrary/Tests/SharedModelsTests/Mocks.swift diff --git a/App/App/App.xctestplan b/App/App/App.xctestplan index 89c3ac0..3db89c3 100644 --- a/App/App/App.xctestplan +++ b/App/App/App.xctestplan @@ -23,13 +23,6 @@ "identifier" : "ScheduleFeatureTests", "name" : "ScheduleFeatureTests" } - }, - { - "target" : { - "containerPath" : "container:..\/MyLibrary", - "identifier" : "SharedModelsTests", - "name" : "SharedModelsTests" - } } ], "version" : 1 diff --git a/MyLibrary/Package.swift b/MyLibrary/Package.swift index fd9917f..43036d9 100644 --- a/MyLibrary/Package.swift +++ b/MyLibrary/Package.swift @@ -104,12 +104,5 @@ let package = Package( .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] ), - .testTarget( - name: "SharedModelsTests", - dependencies: [ - "SharedModels", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] - ), ] ) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index 4291fcb..b8ec09a 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -426,7 +426,7 @@ public struct ScheduleView: View { } } -private extension [SharedModels.Schedule] { +extension [SharedModels.Schedule] { func filtered(using favorites: Favorites, in conference: Conference) -> Self { self .map { diff --git a/MyLibrary/Tests/ScheduleFeatureTests/ScheduleTests.swift b/MyLibrary/Tests/ScheduleFeatureTests/ScheduleTests.swift index f49be0a..db257cf 100644 --- a/MyLibrary/Tests/ScheduleFeatureTests/ScheduleTests.swift +++ b/MyLibrary/Tests/ScheduleFeatureTests/ScheduleTests.swift @@ -115,4 +115,16 @@ final class ScheduleTests: XCTestCase { initialState.favorites = [initialState.day1!.title: [firstSession]] return initialState }() + + @MainActor + func testSchedulesFiltered() { + let schedulesWith2Sessions = [Schedule(time: Date(timeIntervalSince1970: 10_000), sessions: [.mock1, .mock2])] + let conference = Conference(id: 1, title: "conference", date: Date(timeIntervalSince1970: 1_000), schedules: schedulesWith2Sessions) + let favoritesMock1Only: Favorites = [conference.title: [.mock1]] + + let actual = schedulesWith2Sessions.filtered(using: favoritesMock1Only, in: conference) + + let schedulesMock1Only = [Schedule(time: Date(timeIntervalSince1970: 10_000), sessions: [.mock1])] + XCTAssertEqual(actual, schedulesMock1Only) + } } diff --git a/MyLibrary/Tests/SharedModelsTests/ConferenceTests.swift b/MyLibrary/Tests/SharedModelsTests/ConferenceTests.swift deleted file mode 100644 index b254c08..0000000 --- a/MyLibrary/Tests/SharedModelsTests/ConferenceTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -import ComposableArchitecture -import XCTest - -@testable import SharedModels - -final class ConferenceTests: XCTestCase { - @MainActor - func testSchedulesFiltered() { - let schedulesWith2Sessions = [Schedule(time: Date(timeIntervalSince1970: 10_000), sessions: [.mock1, .mock2])] - let conference = Conference(id: 1, title: "conference", date: Date(timeIntervalSince1970: 1_000), schedules: schedulesWith2Sessions) - let favoritesMock1Only = Favorites(eachConferenceFavorites: [(conference, [.mock1])]) - - let actual = schedulesWith2Sessions.filtered(using: favoritesMock1Only, in: conference) - - let schedulesMock1Only = [Schedule(time: Date(timeIntervalSince1970: 10_000), sessions: [.mock1])] - XCTAssertEqual(actual, schedulesMock1Only) - } -} diff --git a/MyLibrary/Tests/SharedModelsTests/Mocks.swift b/MyLibrary/Tests/SharedModelsTests/Mocks.swift deleted file mode 100644 index 4af3d3f..0000000 --- a/MyLibrary/Tests/SharedModelsTests/Mocks.swift +++ /dev/null @@ -1,6 +0,0 @@ -import SharedModels - -extension Session { - static let mock1 = Session(title: "1", speakers: nil, place: nil, description: nil, requirements: nil) - static let mock2 = Session(title: "2", speakers: nil, place: nil, description: nil, requirements: nil) -} From 3d478114ea16abc885d0692cb2e18b6eb8b19e16 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Mon, 1 Apr 2024 00:16:48 +0900 Subject: [PATCH 12/13] change variable names of Conference, day to conference, as other place does --- MyLibrary/Sources/ScheduleFeature/Schedule.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index b8ec09a..649eec3 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -109,7 +109,7 @@ public struct Schedule { ) return .none case let .view(.favoriteIconTapped(session)): - let day = switch state.selectedDay { + let conference = switch state.selectedDay { case .day1: state.day1! case .day2: @@ -118,10 +118,10 @@ public struct Schedule { state.workshop! } var favorites = state.favorites - favorites.updateFavoriteState(of: session, in: day) + favorites.updateFavoriteState(of: session, in: conference) return .run { [favorites = favorites] send in try? fileClient.saveFavorites(favorites) - await send(.savedFavorites(session, day)) + await send(.savedFavorites(session, conference)) } case let .savedFavorites(session, day): state.favorites.updateFavoriteState(of: session, in: day) @@ -392,7 +392,7 @@ public struct ScheduleView: View { case .all: return conference.schedules case .favorite: - let day = + let conference = switch store.selectedDay { case .day1: store.day1! @@ -401,7 +401,7 @@ public struct ScheduleView: View { case .day3: store.workshop! } - return conference.schedules.filtered(using: store.favorites, in: day) + return conference.schedules.filtered(using: store.favorites, in: conference) } } From e1769aa0edb6304bbe1206bb6a3c4009249b2ea0 Mon Sep 17 00:00:00 2001 From: Yusuke Uchida <65172567+ewa1989@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:34:46 +0900 Subject: [PATCH 13/13] fix to enable to test favorite filter via Reducer. --- .../Sources/ScheduleFeature/Schedule.swift | 89 +++++++++++-------- .../Sources/SharedModels/Conference.swift | 2 + .../ScheduleFeatureTests/ScheduleTests.swift | 24 +++-- 3 files changed, 68 insertions(+), 47 deletions(-) diff --git a/MyLibrary/Sources/ScheduleFeature/Schedule.swift b/MyLibrary/Sources/ScheduleFeature/Schedule.swift index 649eec3..196f72e 100644 --- a/MyLibrary/Sources/ScheduleFeature/Schedule.swift +++ b/MyLibrary/Sources/ScheduleFeature/Schedule.swift @@ -36,7 +36,37 @@ public struct Schedule { var favorites: Favorites = [:] @Presents var destination: Destination.State? + var day1ToShow: Conference? { + filteredConference(of: .day1) + } + var day2ToShow: Conference? { + filteredConference(of: .day2) + } + var workshopToShow: Conference? { + filteredConference(of: .day3) + } + public init() {} + + func filteredConference(of day: Days) -> Conference? { + let conference = + switch selectedDay { + case .day1: + day1 + case .day2: + day2 + case .day3: + workshop + } + guard let conference = conference else { return nil } + switch selectedFilter { + case .all: + return conference + case .favorite: + let schedules = conference.schedules.filtered(using: favorites, in: conference) + return Conference(id: conference.id, title: conference.title, date: conference.date, schedules: schedules) + } + } } public enum Action: BindableAction, ViewAction { @@ -170,6 +200,21 @@ private extension Favorites { } } +extension [SharedModels.Schedule] { + func filtered(using favorites: Favorites, in conference: Conference) -> Self { + self + .map { + SharedModels.Schedule(time: $0.time, sessions: $0.sessions.filter { + guard let favorites = favorites[conference.title] else { + return false + } + return favorites.contains($0) + }) + } + .filter { $0.sessions.count > 0 } + } +} + @ViewAction(for: Schedule.self) public struct ScheduleView: View { @@ -204,19 +249,19 @@ public struct ScheduleView: View { .padding(.horizontal) switch store.selectedDay { case .day1: - if let day1 = store.day1 { + if let day1 = store.day1ToShow { conferenceList(conference: day1) } else { Text("") } case .day2: - if let day2 = store.day2 { + if let day2 = store.day2ToShow { conferenceList(conference: day2) } else { Text("") } case .day3: - if let workshop = store.workshop { + if let workshop = store.workshopToShow { conferenceList(conference: workshop) } else { Text("") @@ -253,9 +298,8 @@ public struct ScheduleView: View { Text(conference.date, style: .date) .font(.title2) - let schedules = extractFilteredSchedules(from: conference) - if schedules.count > 0 { - ForEach(schedules, id: \.self) { schedule in + if conference.schedules.count > 0 { + ForEach(conference.schedules, id: \.self) { schedule in VStack(alignment: .leading, spacing: 4) { Text(schedule.time, style: .time) .font(.subheadline.bold()) @@ -387,24 +431,6 @@ public struct ScheduleView: View { } } - func extractFilteredSchedules(from conference: Conference) -> [SharedModels.Schedule] { - switch store.selectedFilter { - case .all: - return conference.schedules - case .favorite: - let conference = - switch store.selectedDay { - case .day1: - store.day1! - case .day2: - store.day2! - case .day3: - store.workshop! - } - return conference.schedules.filtered(using: store.favorites, in: conference) - } - } - func officeHourTitle(speakers: [Speaker]) -> String { let names = givenNameList(speakers: speakers) return String(localized: "Office hour \(names)", bundle: .module) @@ -426,21 +452,6 @@ public struct ScheduleView: View { } } -extension [SharedModels.Schedule] { - func filtered(using favorites: Favorites, in conference: Conference) -> Self { - self - .map { - SharedModels.Schedule(time: $0.time, sessions: $0.sessions.filter { - guard let favorites = favorites[conference.title] else { - return false - } - return favorites.contains($0) - }) - } - .filter { $0.sessions.count > 0 } - } -} - #Preview { ScheduleView( store: .init( diff --git a/MyLibrary/Sources/SharedModels/Conference.swift b/MyLibrary/Sources/SharedModels/Conference.swift index 17c470a..460eada 100644 --- a/MyLibrary/Sources/SharedModels/Conference.swift +++ b/MyLibrary/Sources/SharedModels/Conference.swift @@ -1,11 +1,13 @@ import Foundation public struct Conference: Codable, Equatable, Hashable, Sendable { + public var id: Int public var title: String public var date: Date public var schedules: [Schedule] public init(id: Int, title: String, date: Date, schedules: [Schedule]) { + self.id = id self.title = title self.date = date self.schedules = schedules diff --git a/MyLibrary/Tests/ScheduleFeatureTests/ScheduleTests.swift b/MyLibrary/Tests/ScheduleFeatureTests/ScheduleTests.swift index db257cf..9ab6581 100644 --- a/MyLibrary/Tests/ScheduleFeatureTests/ScheduleTests.swift +++ b/MyLibrary/Tests/ScheduleFeatureTests/ScheduleTests.swift @@ -117,14 +117,22 @@ final class ScheduleTests: XCTestCase { }() @MainActor - func testSchedulesFiltered() { - let schedulesWith2Sessions = [Schedule(time: Date(timeIntervalSince1970: 10_000), sessions: [.mock1, .mock2])] - let conference = Conference(id: 1, title: "conference", date: Date(timeIntervalSince1970: 1_000), schedules: schedulesWith2Sessions) - let favoritesMock1Only: Favorites = [conference.title: [.mock1]] - - let actual = schedulesWith2Sessions.filtered(using: favoritesMock1Only, in: conference) - + func testSchedulesFilteredFavoritesOnly() async { + let initialState: ScheduleFeature.Schedule.State = ScheduleTests.selectingDay1ScheduleWithOneFavorite + let store = TestStore(initialState: initialState) { + Schedule() + } let schedulesMock1Only = [Schedule(time: Date(timeIntervalSince1970: 10_000), sessions: [.mock1])] - XCTAssertEqual(actual, schedulesMock1Only) + let expected = Conference( + id: 1, + title: "conference1", + date: Date(timeIntervalSince1970: 1_000), + schedules: schedulesMock1Only + ) + + await store.send(.binding(.set(\.selectedFilter, Schedule.FilterItem.favorite))) { + $0.selectedFilter = .favorite + XCTAssertEqual($0.day1ToShow, expected) + } } }