-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Check favorite sessions in Schedule View #41
base: main
Are you sure you want to change the base?
Check favorite sessions in Schedule View #41
Conversation
…k-favorite-session-feature # Conflicts: # MyLibrary/Sources/ScheduleFeature/Schedule.swift
public var saveDay1: @Sendable (Conference) throws -> Void | ||
public var saveDay2: @Sendable (Conference) throws -> Void | ||
public var saveWorkshop: @Sendable (Conference) throws -> Void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to create UserDefaultsClient instead of DataClient since this will be changed to ApiClient for example.
Also, favorite data is better to store in Document folder instead of User Default. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, favorite data is better to store in Document folder instead of User Default. What do you think?
I agree for reading UserDefaults documentation saying
The defaults system allows an app to customize its behavior to match a user’s preferences.
Because favorite data are for saving app's state resulting in user operations, rather than customizing app's behavior.
So I will create FileClient or something... I'm thinking appropriate name that explains what it does, not how it does.
If you have any idea, please tell me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case let .view(.favoriteIconTapped(session)): | ||
switch state.selectedDay { | ||
case .day1: | ||
state.day1 = update(state.day1!, togglingFavoriteOf: session) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think saving Conference
object itself is not good approach. This leads such a whole updating object.
Instead, It might be better for creating [Days: Favorite]
, which having session hash (I know session.id
is better but I don't believe id
in handmade-JSON 😶🌫️ ).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead, It might be better for creating [Days: Favorite], which having session hash
Thank you.
I've chosen easier way came up with, but will try better approach of performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To create save method for [Days: Favorite]
, Days
must be shown from DataClient module.
However Days
is a type of LocalizedStringKey
that is a frozen struct of SwiftUI
.
Therefore I think it might be better to create save method for [Session: Bool]
, what do you think?
Further background...
- If putting
Days
inSharedModels
and save[Days: [Sesson]]
SharedModels
depends onSwiftUI
, I thinkSharedModels
wants to depends only onFoundation
.
- If putting
Days
still inScheduleFeature
and make it publicDataClient
usesDays
for determining save method, and of courseScheduleFeature
depends onDataClient
already, so there's circular dependence.
Therefore, I resulted in it's better to create [Session: Bool]
as Favorites
in SharedModels
, what do you think of this design idea?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I realized that it's better to use Conference
as key instead of enum Days
!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ewa1989 Also please add unit tests to ensure that the favorite functions are working properly. |
FYI: Now Map is move to tab so may conflict your PR. |
…tching methods which might change API Client in future
# Conflicts: # MyLibrary/Sources/ScheduleFeature/Schedule.swift
…se favorite data aren't customizing app's behavior
@@ -49,9 +49,3 @@ extension DataClient: DependencyKey { | |||
return data | |||
} | |||
} | |||
|
|||
let jsonDecoder = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still we need to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ask you create in another module. Define in Package.swift.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not have to create in another file. Just put in under the client code even if duplicated.
public struct Favorites: Equatable, Codable { | ||
private var eachConferenceFavorites: [String: [Session]] | ||
|
||
public init(eachConferenceFavorites: [(conference: Conference, favoriteSessions: [Session])]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since initialized in second line, first parameter should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for my code readability, but first parameter is necessary.
To create Favorites
, give pairs which has conference and its favorites sessions.
Then convert given pairs to dictionary of [conference title: its favorites sessions], and set the dictionary to property.
I intentionally use conference as input to avoid using non-existent conference title as key.
I'll continue to think readable name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to use reduce(into:)
self.favorites = favorites.reduce(into: [:]) { result, element in
result[element.conference.title] = element.favoriteSessions
}
@@ -95,3 +97,28 @@ extension Speaker { | |||
] | |||
) | |||
} | |||
|
|||
extension Favorites { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can add mock under the your test code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved these specific mocks near related tests in 4e279e4.
@@ -13,12 +14,14 @@ final class ScheduleTests: XCTestCase { | |||
$0[DataClient.self].fetchDay1 = { @Sendable in .mock1 } | |||
$0[DataClient.self].fetchDay2 = { @Sendable in .mock2 } | |||
$0[DataClient.self].fetchWorkshop = { @Sendable in .mock3 } | |||
$0[FileClient.self].loadFavorites = { @Sendable in .mock1 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be separated your test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry that I can't image way to separate.
Could you give me more idea?
Current implementation, loading favorites using FileClient
and fetching conferences using DataClient
at the same time (onAppear
).
So it's required to implement testValue
of loadFavorites
to test onAppear
behavior.
I could only image to separate loadResponse(Result<Favorites, Error>)
from fetchResponse(Result<SchedulesResponse, Error>)
, but this idea also requires testValue
of loadFavorites
, so I think it doesn't satisfy your review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NO. This effects to current test case. Should be created another test case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since fetching data using DataClient
and loading favorites using FileClient
are must to be executed at the same time, onAppear, I renamed testFetchData
to testOnAppear
in 04cefc0.
} | ||
|
||
@ViewBuilder | ||
func favoriteIcon(for session: Session) -> some View { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These logic should put in Reducer so that we can write test easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry that I can't image which part of logic to put in Reducer.
Does it mean create new Schedule.Action
case and handle it in Reduce
?
Or create computed property getting Conference
in Schedule.State
?
Now I implemented Favorites
to use Conference.title
as key for getting value of favorited [Session]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved these logic to Schedule.swift
in d3b9901.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this method if you have moved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for my lack of explanation.
I moved a part of logic as far as possible, so now State.favorites: [String: [Session]]
is used in view whether each session is favorited or not, only.
This reverts commit f2622b8.
# Conflicts: # MyLibrary/Package.swift # MyLibrary/Sources/ScheduleFeature/Schedule.swift
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forget to comment you since stacked in pending 😅
public struct Favorites: Equatable, Codable { | ||
private var eachConferenceFavorites: [String: [Session]] | ||
|
||
public init(eachConferenceFavorites: [(conference: Conference, favoriteSessions: [Session])]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to use reduce(into:)
self.favorites = favorites.reduce(into: [:]) { result, element in
result[element.conference.title] = element.favoriteSessions
}
public mutating func updateFavoriteState(of session: Session, in conference: Conference) { | ||
guard var favorites = eachConferenceFavorites[conference.title] else { | ||
eachConferenceFavorites[conference.title] = [session] | ||
return | ||
} | ||
if favorites.contains(session) { | ||
eachConferenceFavorites[conference.title] = favorites.filter { $0 != session } | ||
} else { | ||
favorites.append(session) | ||
eachConferenceFavorites[conference.title] = favorites | ||
} | ||
} | ||
|
||
public func isFavorited(_ session: Session, in conference: Conference) -> Bool { | ||
guard let favorites = eachConferenceFavorites[conference.title] else { | ||
return false | ||
} | ||
return favorites.contains(session) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move into reducer. Favorite should be keep as pure model.
@@ -13,12 +14,14 @@ final class ScheduleTests: XCTestCase { | |||
$0[DataClient.self].fetchDay1 = { @Sendable in .mock1 } | |||
$0[DataClient.self].fetchDay2 = { @Sendable in .mock2 } | |||
$0[DataClient.self].fetchWorkshop = { @Sendable in .mock3 } | |||
$0[FileClient.self].loadFavorites = { @Sendable in .mock1 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NO. This effects to current test case. Should be created another test case.
} | ||
) | ||
|
||
static func saveDataToFile(_ data : Data, named fileName: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static func saveDataToFile(_ data : Data, named fileName: String) { | |
static func deserialize(data : Data, into fileName: String) { |
try? data.write(to: fileURL) | ||
} | ||
|
||
static func loadDataFromFile(named fileName: String) -> Data? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static func loadDataFromFile(named fileName: String) -> Data? { | |
static func serialize(from filePath: String) -> Data? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed them to explain exactly how they do in cfb2fa3.
Please prioritize what I said to you in-person. Thank you! |
I remember your advice at the conference, that was so thankful! |
…and View, and remove Favorites and use typealias instead because Favorites has one property only.
@d-date |
return .run { send in | ||
await send( | ||
.fetchResponse( | ||
Result { | ||
let day1 = try dataClient.fetchDay1() | ||
let day2 = try dataClient.fetchDay2() | ||
let workshop = try dataClient.fetchWorkshop() | ||
return .init(day1: day1, day2: day2, workshop: workshop) | ||
})) | ||
await send( | ||
.loadResponse( | ||
Result { | ||
try fileClient.loadFavorites() | ||
})) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to change response to Result<(SchedulesResponse, Favorites), Error>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, fixed simpler in 0e3b9f8.
} | ||
|
||
@ViewBuilder | ||
func favoriteIcon(for session: Session) -> some View { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this method if you have moved.
Add check favorite session feature as below.
Ref: #40
Note
[String: [Session]]
in DocumentConference
including favorited stateusing UserDefaultsas Json files, in save methods ofFileClient
DataClient
[String: [Session]]
from DocumentConference
UserDefaults, and if failed then load from files in Resources