Add animation to showing favourite star image on timeline

This commit is contained in:
Marcin Czachursk 2023-04-15 08:03:16 +02:00
parent dca985dcf8
commit 2fcd7bcd3c
8 changed files with 176 additions and 221 deletions

View File

@ -158,7 +158,6 @@
F89D6C4629718193001DA3D4 /* GeneralSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4529718193001DA3D4 /* GeneralSectionView.swift */; };
F89D6C4A297196FF001DA3D4 /* ImageViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C49297196FF001DA3D4 /* ImageViewer.swift */; };
F89F57B029D1C11200001EE3 /* RelationshipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */; };
F8A4A88329E3FD1C00267E36 /* ImageAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */; };
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
F8AFF7C129B259150087D083 /* HashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C029B259150087D083 /* HashtagsView.swift */; };
F8AFF7C429B25EF40087D083 /* ImagesGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AFF7C329B25EF40087D083 /* ImagesGrid.swift */; };
@ -176,7 +175,6 @@
F8F6E44D29BCC1F90004795E /* MediumWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44829BCC0F00004795E /* MediumWidgetView.swift */; };
F8F6E44E29BCC1FB0004795E /* LargeWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44A29BCC0FF0004795E /* LargeWidgetView.swift */; };
F8F6E45129BCE9190004795E /* UIImage+Resize.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E45029BCE9190004795E /* UIImage+Resize.swift */; };
F8FFBD4829E9901E0047EE80 /* ImageFavourite.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FFBD4729E9901E0047EE80 /* ImageFavourite.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -330,7 +328,6 @@
F89D6C49297196FF001DA3D4 /* ImageViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewer.swift; sourceTree = "<group>"; };
F89F0605299139F6003DC875 /* Vernissage-002.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-002.xcdatamodel"; sourceTree = "<group>"; };
F89F57AF29D1C11200001EE3 /* RelationshipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationshipModel.swift; sourceTree = "<group>"; };
F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAvatar.swift; sourceTree = "<group>"; };
F8A4A88429E4099900267E36 /* Vernissage-008.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-008.xcdatamodel"; sourceTree = "<group>"; };
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
F8AFF7C029B259150087D083 /* HashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagsView.swift; sourceTree = "<group>"; };
@ -358,7 +355,6 @@
F8F6E44829BCC0F00004795E /* MediumWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediumWidgetView.swift; sourceTree = "<group>"; };
F8F6E44A29BCC0FF0004795E /* LargeWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeWidgetView.swift; sourceTree = "<group>"; };
F8F6E45029BCE9190004795E /* UIImage+Resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Resize.swift"; sourceTree = "<group>"; };
F8FFBD4729E9901E0047EE80 /* ImageFavourite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFavourite.swift; sourceTree = "<group>"; };
F8FFBD4929E99BEE0047EE80 /* Vernissage-009.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-009.xcdatamodel"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -638,8 +634,6 @@
isa = PBXGroup;
children = (
F88BC53A29E06A5100CE6141 /* ImageContextMenu.swift */,
F8A4A88229E3FD1C00267E36 /* ImageAvatar.swift */,
F8FFBD4729E9901E0047EE80 /* ImageFavourite.swift */,
);
path = ViewModifiers;
sourceTree = "<group>";
@ -1080,9 +1074,7 @@
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */,
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */,
F8FFBD4829E9901E0047EE80 /* ImageFavourite.swift in Sources */,
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */,
F8A4A88329E3FD1C00267E36 /* ImageAvatar.swift in Sources */,
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
F891E7D029C368750022C449 /* ImageRowItemAsync.swift in Sources */,

View File

@ -1,83 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
import NukeUI
import ClientKit
import ServicesKit
import EnvironmentKit
public extension View {
func imageAvatar(displayName: String?, avatarUrl: URL?) -> some View {
modifier(ImageAvatar(displayName: displayName, avatarUrl: avatarUrl))
}
}
private struct ImageAvatar: ViewModifier {
@EnvironmentObject var applicationState: ApplicationState
private let displayName: String?
private let avatarUrl: URL?
init(displayName: String?, avatarUrl: URL?) {
self.displayName = displayName
self.avatarUrl = avatarUrl
}
func body(content: Content) -> some View {
if self.applicationState.showAvatarsOnTimeline {
ZStack {
// Image.
content
// Avatar.
VStack(alignment: .leading) {
HStack(alignment: .center) {
LazyImage(url: avatarUrl) { state in
if let image = state.image {
self.buildAvatar(image: image)
} else if state.isLoading {
self.buildAvatar()
} else {
self.buildAvatar()
}
}
Text(displayName ?? "")
.font(.system(size: 15))
.foregroundColor(.white.opacity(0.8))
.fontWeight(.semibold)
.shadow(color: .black, radius: 2)
Spacer()
}
Spacer()
}
.padding(.leading, 8)
.padding(.top, 8)
}
} else {
content
}
}
@ViewBuilder
private func buildAvatar(image: Image? = nil) -> some View {
(image ?? Image("Avatar"))
.resizable()
.clipShape(applicationState.avatarShape.shape())
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
.overlay(
applicationState.avatarShape.shape()
.stroke(Color.white.opacity(0.6), lineWidth: 1)
.frame(width: 24, height: 24)
)
.shadow(color: .black, radius: 2)
}
}

View File

@ -1,53 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
import NukeUI
import ClientKit
import ServicesKit
import EnvironmentKit
public extension View {
func imageFavourite(isFavourited: Binding<Bool>) -> some View {
modifier(ImageFavourite(isFavourited: isFavourited))
}
}
private struct ImageFavourite: ViewModifier {
@EnvironmentObject var applicationState: ApplicationState
@Binding private var isFavourited: Bool
init(isFavourited: Binding<Bool>) {
self._isFavourited = isFavourited
}
func body(content: Content) -> some View {
if self.applicationState.showFavouritesOnTimeline && self.isFavourited {
ZStack {
// Image.
content
// Avatar.
VStack(alignment: .leading) {
Spacer()
HStack(alignment: .center) {
Image(systemName: "star.fill")
.font(.system(size: 12))
.shadow(color: .black, radius: 4)
.foregroundColor(.white.opacity(0.8))
Spacer()
}
}
.padding(.leading, 12)
.padding(.bottom, 14)
}
} else {
content
}
}
}

View File

@ -42,20 +42,15 @@ struct ImageRowItem: View {
if self.status.sensitive && !self.applicationState.showSensitive {
ZStack {
ContentWarning(spoilerText: self.status.spoilerText) {
self.imageView(uiImage: uiImage)
if showThumbImage {
FavouriteTouch {
self.showThumbImage = false
}
}
self.imageContainerView(uiImage: uiImage)
} blurred: {
BlurredImage(blurhash: attachmentData.blurhash)
.imageAvatar(displayName: self.status.accountDisplayName,
avatarUrl: self.status.accountAvatar)
.onTapGesture {
self.navigateToStatus()
}
ZStack {
BlurredImage(blurhash: attachmentData.blurhash)
ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar)
}
.onTapGesture {
self.navigateToStatus()
}
}
}
.opacity(self.opacity)
@ -66,13 +61,7 @@ struct ImageRowItem: View {
}
} else {
ZStack {
self.imageView(uiImage: uiImage)
if showThumbImage {
FavouriteTouch {
self.showThumbImage = false
}
}
self.imageContainerView(uiImage: uiImage)
}
.opacity(self.opacity)
.onAppear {
@ -108,6 +97,15 @@ struct ImageRowItem: View {
}
}
@ViewBuilder
private func imageContainerView(uiImage: UIImage) -> some View {
self.imageView(uiImage: uiImage)
ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar)
ImageFavourite(isFavourited: $isFavourited)
FavouriteTouch(showFavouriteAnimation: $showThumbImage)
}
@ViewBuilder
private func imageView(uiImage: UIImage) -> some View {
Image(uiImage: uiImage)
@ -129,14 +127,13 @@ struct ImageRowItem: View {
HapticService.shared.fireHaptic(of: .buttonPress)
// Mark favourite booleans used to show star in the timeline view.
self.isFavourited = true
withAnimation(.default.delay(2.0)) {
self.isFavourited = true
}
}
.onTapGesture {
self.navigateToStatus()
}
.imageAvatar(displayName: self.status.accountDisplayName,
avatarUrl: self.status.accountAvatar)
.imageFavourite(isFavourited: $isFavourited)
.imageContextMenu(statusData: self.status)
.onAppear {
self.isFavourited = self.status.favourited

View File

@ -42,21 +42,15 @@ struct ImageRowItemAsync: View {
if self.statusViewModel.sensitive && !self.applicationState.showSensitive {
ZStack {
ContentWarning(spoilerText: self.statusViewModel.spoilerText) {
self.imageView(image: image)
self.imageContainerView(image: image)
} blurred: {
BlurredImage(blurhash: attachment.blurhash)
.if(self.showAvatar) {
$0.imageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis,
avatarUrl: self.statusViewModel.account.avatar)
}
.onTapGesture {
self.navigateToStatus()
}
}
if showThumbImage {
FavouriteTouch {
self.showThumbImage = false
ZStack {
BlurredImage(blurhash: attachment.blurhash)
ImageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis,
avatarUrl: self.statusViewModel.account.avatar)
}
.onTapGesture {
self.navigateToStatus()
}
}
}
@ -72,13 +66,7 @@ struct ImageRowItemAsync: View {
}
} else {
ZStack {
self.imageView(image: image)
if showThumbImage {
FavouriteTouch {
self.showThumbImage = false
}
}
self.imageContainerView(image: image)
}
.opacity(self.opacity)
.onAppear {
@ -116,6 +104,19 @@ struct ImageRowItemAsync: View {
.priority(.high)
}
@ViewBuilder
private func imageContainerView(image: Image) -> some View {
self.imageView(image: image)
if self.showAvatar {
ImageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis,
avatarUrl: self.statusViewModel.account.avatar)
}
ImageFavourite(isFavourited: $isFavourited)
FavouriteTouch(showFavouriteAnimation: $showThumbImage)
}
@ViewBuilder
private func imageView(image: Image) -> some View {
image
@ -133,16 +134,13 @@ struct ImageRowItemAsync: View {
// Mark favourite booleans used to show star in the timeline view.
self.statusViewModel.favourited = true
self.isFavourited = true
withAnimation(.default.delay(2.0)) {
self.isFavourited = true
}
}
.onTapGesture {
self.navigateToStatus()
}
.if(self.showAvatar) {
$0.imageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis,
avatarUrl: self.statusViewModel.account.avatar)
}
.imageFavourite(isFavourited: $isFavourited)
.imageContextMenu(statusModel: self.statusViewModel)
.onAppear {
self.isFavourited = self.statusViewModel.favourited

View File

@ -11,41 +11,43 @@ public struct FavouriteTouch: View {
@State private var showCircle = 0
@State private var opacity = 1.0
private let finished: () -> Void
@Binding private var showFavouriteAnimation: Bool
public init(finished: @escaping () -> Void) {
self.finished = finished
public init(showFavouriteAnimation: Binding<Bool>) {
self._showFavouriteAnimation = showFavouriteAnimation
}
public var body: some View {
ZStack {
Circle()
.frame(width: 55, height: 55, alignment: .center)
.foregroundColor(.white.opacity(0.75))
.scaleEffect(CGFloat(showCircle))
if self.showFavouriteAnimation {
ZStack {
Circle()
.frame(width: 55, height: 55, alignment: .center)
.foregroundColor(.white.opacity(0.75))
.scaleEffect(CGFloat(showCircle))
Image(systemName: "star.fill")
.font(.system(size: 26))
.foregroundColor(.black.opacity(0.4))
.clipShape(Rectangle().offset(y: CGFloat(showThumb)))
}
.opacity(opacity)
.onAppear {
withAnimation(Animation.interpolatingSpring(stiffness: 170, damping: 15)) {
showCircle = 1
Image(systemName: "star.fill")
.font(.system(size: 26))
.foregroundColor(.black.opacity(0.4))
.clipShape(Rectangle().offset(y: CGFloat(showThumb)))
}
.opacity(opacity)
.onAppear {
withAnimation(Animation.interpolatingSpring(stiffness: 170, damping: 15)) {
showCircle = 1
}
withAnimation(Animation.easeInOut(duration: 0.5).delay(0.25)) {
showThumb = 0
}
withAnimation(Animation.easeInOut(duration: 0.5).delay(0.25)) {
showThumb = 0
}
withAnimation(Animation.easeInOut(duration: 0.5).delay(1.75)) {
opacity = 0
withAnimation(Animation.easeInOut(duration: 0.5).delay(1.75)) {
opacity = 0
}
}
.task {
try? await Task.sleep(nanoseconds: 2_500_000_000)
self.showFavouriteAnimation = false
}
}
.task {
try? await Task.sleep(nanoseconds: 2_500_000_000)
self.finished()
}
}
}

View File

@ -0,0 +1,66 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
import NukeUI
import EnvironmentKit
public struct ImageAvatar: View {
@EnvironmentObject var applicationState: ApplicationState
private let displayName: String?
private let avatarUrl: URL?
public init(displayName: String?, avatarUrl: URL?) {
self.displayName = displayName
self.avatarUrl = avatarUrl
}
public var body: some View {
if self.applicationState.showAvatarsOnTimeline {
VStack(alignment: .leading) {
HStack(alignment: .center) {
LazyImage(url: avatarUrl) { state in
if let image = state.image {
self.buildAvatar(image: image)
} else if state.isLoading {
self.buildAvatar()
} else {
self.buildAvatar()
}
}
Text(displayName ?? "")
.font(.system(size: 15))
.foregroundColor(.white.opacity(0.8))
.fontWeight(.semibold)
.shadow(color: .black, radius: 2)
Spacer()
}
Spacer()
}
.padding(.leading, 8)
.padding(.top, 8)
}
}
@ViewBuilder
private func buildAvatar(image: Image? = nil) -> some View {
(image ?? Image("Avatar"))
.resizable()
.clipShape(applicationState.avatarShape.shape())
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
.overlay(
applicationState.avatarShape.shape()
.stroke(Color.white.opacity(0.6), lineWidth: 1)
.frame(width: 24, height: 24)
)
.shadow(color: .black, radius: 2)
}
}

View File

@ -0,0 +1,36 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
import EnvironmentKit
public struct ImageFavourite: View {
@EnvironmentObject var applicationState: ApplicationState
@Binding private var isFavourited: Bool
public init(isFavourited: Binding<Bool>) {
self._isFavourited = isFavourited
}
public var body: some View {
if self.applicationState.showFavouritesOnTimeline && self.isFavourited {
VStack(alignment: .leading) {
Spacer()
HStack(alignment: .center) {
Image(systemName: "star.fill")
.font(.system(size: 12))
.shadow(color: .black, radius: 4)
.foregroundColor(.white.opacity(0.8))
Spacer()
}
}
.padding(.leading, 12)
.padding(.bottom, 14)
}
}
}