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

[iOS] Add animation when you tap heartmark in timetable list. #944

Merged
53 changes: 37 additions & 16 deletions app-ios/Sources/CommonComponents/Timetable/TimetableCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@ public struct TimetableCard: View {
let timetableItem: TimetableItem
let isFavorite: Bool
let onTap: (TimetableItem) -> Void
let onTapFavorite: (TimetableItem) -> Void
let onTapFavorite: (TimetableItem, CGPoint?) -> Void

public init(
timetableItem: TimetableItem,
isFavorite: Bool,
onTap: @escaping (TimetableItem) -> Void,
onTapFavorite: @escaping (TimetableItem) -> Void
onTapFavorite: @escaping (TimetableItem, CGPoint?) -> Void
) {
self.timetableItem = timetableItem
self.isFavorite = isFavorite
self.onTap = onTap
self.onTapFavorite = onTapFavorite
}

// MEMO: Used to adjust margins by Orientation.
@Environment(\.horizontalSizeClass) var heightSizeClass
@Environment(\.verticalSizeClass) var widthSizeClass

public var body: some View {
Button {
onTap(timetableItem)
Expand All @@ -35,20 +39,29 @@ public struct TimetableCard: View {
LanguageTag(label)
}
Spacer()
Button {
onTapFavorite(timetableItem)
} label: {
Image(isFavorite ? .icFavoriteFill : .icFavoriteOutline)
.resizable()
.renderingMode(.template)
.foregroundColor(
isFavorite ?
AssetColors.Primary.primaryFixed.swiftUIColor
:
AssetColors.Surface.onSurfaceVariant.swiftUIColor
)
.frame(width: 24, height: 24)

// [NOTE] In order to calculate the value from GeometryReader, it is supported by assigning DragGesture to the Image element instead of Button.
HStack {
GeometryReader { geometry in
// MEMO: Since the coordinate values ​​are based on the inside of the View, ".local" is specified.
let localGeometry = geometry.frame(in: .local)
Image(isFavorite ? .icFavoriteFill : .icFavoriteOutline)
.resizable()
.renderingMode(.template)
.foregroundColor(
isFavorite ? AssetColors.Primary.primaryFixed.swiftUIColor : AssetColors.Surface.onSurfaceVariant.swiftUIColor
)
.frame(width: 24, height: 24)
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { dragGesture in
// MEMO: The offset value in the Y-axis direction is subtracted for adjustment (decided by device orientation).
let adjustedLocationPoint = CGPoint(x: dragGesture.location.x, y: dragGesture.location.y - calculateTopMarginByDevideOrietation())
onTapFavorite(timetableItem, adjustedLocationPoint)
})
// MEMO: To adjust horizontal position, I'm subtracting half the size of Image (-12).
.position(x: localGeometry.maxX - 12, y: localGeometry.midY)
}
}
.frame(height: 24, alignment: .trailing)
.sensoryFeedback(.impact, trigger: isFavorite) { _, newValue in newValue }
}

Expand Down Expand Up @@ -85,6 +98,14 @@ public struct TimetableCard: View {
.overlay(RoundedRectangle(cornerRadius: 4).stroke(AssetColors.Outline.outlineVariant.swiftUIColor, lineWidth: 1))
}
}

private func calculateTopMarginByDevideOrietation() -> CGFloat {
if widthSizeClass == .regular && heightSizeClass == .compact {
return CGFloat(128)
} else {
return CGFloat(96)
}
}
}

#Preview {
Expand All @@ -93,7 +114,7 @@ public struct TimetableCard: View {
timetableItem: TimetableItem.Session.companion.fake(),
isFavorite: true,
onTap: { _ in },
onTapFavorite: { _ in }
onTapFavorite: { _,_ in }
)
.padding(.horizontal, 16)
}
Expand Down
2 changes: 1 addition & 1 deletion app-ios/Sources/FavoriteFeature/FavoriteView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public struct FavoriteView: View {
isFavorite: timetableItemWithFavorite.isFavorited
) { _ in
store.send(.view(.timetableItemTapped(timetableItemWithFavorite)))
} onTapFavorite: { _ in
} onTapFavorite: { _, _ in
store.send(.view(.toggleFavoriteTapped(timetableItem.id)))
}
}
Expand Down
2 changes: 1 addition & 1 deletion app-ios/Sources/SearchFeature/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public struct SearchView: View {
isFavorite: timetableItemWithFavorite.isFavorited
) { _ in
store.send(.view(.timetableItemTapped(timetableItemWithFavorite)))
} onTapFavorite: { _ in
} onTapFavorite: { _, _ in
store.send(.view(.toggleFavoriteTapped(timetableItem.id)))
}
}
Expand Down
105 changes: 87 additions & 18 deletions app-ios/Sources/TimetableFeature/TimetableListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,91 @@
self.store = store
}

// MEMO: A variable that stores the value of Animation variation. (Only 0 or 1)
@State private var animationProgress: CGFloat = 0
// MEMO: Select target targetTimetableItemId & targetLocationPoint (for Animation).
@State private var targetTimetableItemId: TimetableItemId?
@State private var targetLocationPoint: CGPoint?

var body: some View {
ScrollView{
LazyVStack(spacing: 0) {
ForEach(store.timetableItems, id: \.self) { item in
TimeGroupMiniList(contents: item, onItemTap: { item in
store.send(.view(.timetableItemTapped(item)))
}, onFavoriteTap: {
store.send(.view(.favoriteTapped($0)))
})
}
}.scrollContentBackground(.hidden)
.onAppear {
store.send(.view(.onAppear))
}.background(AssetColors.Surface.surface.swiftUIColor)

bottomTabBarPadding
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(store.timetableItems, id: \.self) { item in
TimeGroupMiniList(contents: item, onItemTap: { item in
store.send(.view(.timetableItemTapped(item)))
}, onFavoriteTap: { timetableItemWithFavorite, adjustedLocationPoint in

store.send(.view(.favoriteTapped(timetableItemWithFavorite)))

// MEMO: When "isFavorited" flag is false, this view executes animation.
if timetableItemWithFavorite.isFavorited == false {
toggleFavorite(timetableItem: timetableItemWithFavorite.timetableItem, adjustedLocationPoint: adjustedLocationPoint)
}
})
}
}.scrollContentBackground(.hidden)
.onAppear {
store.send(.view(.onAppear))
}.background(AssetColors.Surface.surface.swiftUIColor)
bottomTabBarPadding
}

// MEMO: Stack the Image elements that will be animated using ZStack.
makeHeartAnimationView()
}
}

@ViewBuilder
private func makeHeartAnimationView() -> some View {
GeometryReader { geometry in
if targetTimetableItemId != nil {
Image(systemName: "heart.fill")
.foregroundColor(
AssetColors.Primary.primaryFixed.swiftUIColor
)
.frame(width: 24, height: 24)
.position(animationPosition(geometry: geometry))
.opacity(1 - animationProgress)
.zIndex(99)
}
}
}

private func animationPosition(geometry: GeometryProxy) -> CGPoint {

// MEMO: Get the value calculated from both the default and .global GeometryReader.
let globalGeometrySize = geometry.frame(in: .global).size
let defaultGeometrySize = geometry.size

// MEMO: Calculate the offset value in the Y-axis direction using GeometryReader.
let startPositionY = targetLocationPoint?.y ?? 0
let endPositionY = defaultGeometrySize.height - 25
let targetY = startPositionY + (endPositionY - startPositionY) * animationProgress

// MEMO: Calculate the offset value in the X-axis direction using GeometryReader.
let adjustedPositionX = animationProgress * (globalGeometrySize.width / 2 - globalGeometrySize.width + 50)
let targetX = defaultGeometrySize.width - 50 + adjustedPositionX

return CGPoint(x: targetX, y: targetY)
}

private func toggleFavorite(timetableItem: TimetableItem, adjustedLocationPoint: CGPoint?) {

targetLocationPoint = adjustedLocationPoint
targetTimetableItemId = timetableItem.id

// MEMO: Execute animation.
if targetTimetableItemId != nil {
withAnimation(.easeOut(duration: 1)) {
animationProgress = 1
}
Task {
try await Task.sleep(nanoseconds: 1_000_000_000)
targetTimetableItemId = nil

Check warning on line 172 in app-ios/Sources/TimetableFeature/TimetableListView.swift

View workflow job for this annotation

GitHub Actions / build-test-ios

capture of 'self' with non-sendable type 'TimetableListView' in a `@Sendable` closure
Copy link
Member

Choose a reason for hiding this comment

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

Can you fix this warning? 🙏🏼

capture of 'self' with non-sendable type 'TimetableListView' in a @Sendable closure

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ry-itto I think you can solve it like this.

@MainActor
struct TimetableListView: View { ... }

targetLocationPoint = nil
animationProgress = 0
}
}
}
}
Expand Down Expand Up @@ -176,7 +245,7 @@
struct TimeGroupMiniList: View {
let contents: TimetableTimeGroupItems
let onItemTap: (TimetableItemWithFavorite) -> Void
let onFavoriteTap: (TimetableItemWithFavorite) -> Void
let onFavoriteTap: (TimetableItemWithFavorite, CGPoint?) -> Void

var body: some View {
HStack(spacing: 16) {
Expand All @@ -194,8 +263,8 @@
onTap: {_ in
onItemTap(item)
},
onTapFavorite: { _ in
onFavoriteTap(item)
onTapFavorite: { _, point in
onFavoriteTap(item, point)
})
}
}
Expand Down
Loading