Skip to content
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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from

Conversation

ewa1989
Copy link

@ewa1989 ewa1989 commented Mar 18, 2024

Add check favorite session feature as below.

Only in ShceduleView, show star icons on the right inside of session row, toggle when the icons are tapped, and persist favorited states.

Ref: #40

Note

  • persist [String: [Session]] Conference including favorited state in Document using UserDefaults as Json files, in save methods of FileClient DataClient
  • when app launch, try to load [String: [Session]] Conference from Document UserDefaults, and if failed then load from files in Resources

…k-favorite-session-feature

# Conflicts:
#	MyLibrary/Sources/ScheduleFeature/Schedule.swift
@d-date d-date changed the title Add check favorite session feature in ShceduleView Check favorite sessions in Schedule View Mar 19, 2024
Comment on lines 13 to 15
public var saveDay1: @Sendable (Conference) throws -> Void
public var saveDay2: @Sendable (Conference) throws -> Void
public var saveWorkshop: @Sendable (Conference) throws -> Void
Copy link
Member

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?

Copy link
Author

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.

Copy link
Author

@ewa1989 ewa1989 Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d-date I created FileClient in 23983b5 and changed save/load area to Document in 0e8b403.
I'll change failed test and create new test for star icon behavior from now.

case let .view(.favoriteIconTapped(session)):
switch state.selectedDay {
case .day1:
state.day1 = update(state.day1!, togglingFavoriteOf: session)
Copy link
Member

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 😶‍🌫️ ).

Copy link
Author

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.

Copy link
Author

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...

  1. If putting Days in SharedModels and save [Days: [Sesson]]
    • SharedModels depends on SwiftUI, I think SharedModels wants to depends only on Foundation.
  2. If putting Days still in ScheduleFeature and make it public
    • DataClient uses Days for determining save method, and of course ScheduleFeature depends on DataClient 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?

Copy link
Author

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!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d-date In 846098f , changed save data type to [String: [Session]] which key is enforced to get from Conference.title.

@d-date
Copy link
Member

d-date commented Mar 19, 2024

@ewa1989 Also please add unit tests to ensure that the favorite functions are working properly.

@d-date
Copy link
Member

d-date commented Mar 20, 2024

FYI: Now Map is move to tab so may conflict your PR.

@ewa1989
Copy link
Author

ewa1989 commented Mar 20, 2024

@d-date

@ewa1989 Also please add unit tests to ensure that the favorite functions are working properly.

Added in 35b4d14.
Code fixing completed!

@@ -49,9 +49,3 @@ extension DataClient: DependencyKey {
return data
}
}

let jsonDecoder = {
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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])]) {
Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

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 {
Copy link
Member

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.

Copy link
Author

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 }
Copy link
Member

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.

Copy link
Author

@ewa1989 ewa1989 Mar 21, 2024

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.

Copy link
Member

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.

Copy link
Author

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 {
Copy link
Member

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.

Copy link
Author

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].

Copy link
Author

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.

Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

@d-date d-date left a 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])]) {
Copy link
Member

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
}

Comment on lines 11 to 30
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)
}
}
Copy link
Member

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 }
Copy link
Member

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static func loadDataFromFile(named fileName: String) -> Data? {
static func serialize(from filePath: String) -> Data? {

Copy link
Author

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.

@d-date
Copy link
Member

d-date commented Mar 26, 2024

Please prioritize what I said to you in-person. Thank you!

@ewa1989
Copy link
Author

ewa1989 commented Mar 26, 2024

I remember your advice at the conference, that was so thankful!
I will do my best to contribute.

…and View, and remove Favorites and use typealias instead because Favorites has one property only.
@ewa1989
Copy link
Author

ewa1989 commented Mar 31, 2024

@d-date
Thank you for your kind review.
I updated the code to mainly focus on conversation with you at the conference, as you said.
As I told, I want to contribute this feature through to the end!

Comment on lines 75 to 89
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()
}))
}
Copy link
Member

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> .

Copy link
Author

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 {
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants