feat: 瀑布流

This commit is contained in:
jinyaqiu 2025-09-02 19:10:39 +08:00
parent 68582b82cb
commit 4836c1f4ae
3 changed files with 133 additions and 51 deletions

View File

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
AB6693CA2E65C94400BCAAC1 /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = AB6693C92E65C94400BCAAC1 /* SVGKit */; };
AB6693CC2E65C94400BCAAC1 /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */; };
AB6695272E67015600BCAAC1 /* WaterfallGrid in Frameworks */ = {isa = PBXBuildFile; productRef = AB6695262E67015600BCAAC1 /* WaterfallGrid */; };
AB8773632E4E04400071CB53 /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = AB8773622E4E040E0071CB53 /* .gitignore */; };
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = ABC150C02E5DB39A00A1F970 /* Lottie */; };
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = ABE8998D2E533A7100CD7BA6 /* Alamofire */; };
@ -62,6 +63,7 @@
AB6693CC2E65C94400BCAAC1 /* SVGKitSwift in Frameworks */,
AB6693CA2E65C94400BCAAC1 /* SVGKit in Frameworks */,
ABE8998E2E533A7100CD7BA6 /* Alamofire in Frameworks */,
AB6695272E67015600BCAAC1 /* WaterfallGrid in Frameworks */,
ABC150C12E5DB39A00A1F970 /* Lottie in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -120,6 +122,7 @@
ABC150C02E5DB39A00A1F970 /* Lottie */,
AB6693C92E65C94400BCAAC1 /* SVGKit */,
AB6693CB2E65C94400BCAAC1 /* SVGKitSwift */,
AB6695262E67015600BCAAC1 /* WaterfallGrid */,
);
productName = wake;
productReference = ABB4E2082E4B75D900660198 /* wake.app */;
@ -153,6 +156,7 @@
ABE8998C2E533A7100CD7BA6 /* XCRemoteSwiftPackageReference "Alamofire" */,
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */,
AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */,
AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = ABB4E2092E4B75D900660198 /* Products */;
@ -409,6 +413,14 @@
minimumVersion = 3.0.0;
};
};
AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/paololeonardi/WaterfallGrid.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.0;
};
};
ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/airbnb/lottie-spm.git";
@ -438,6 +450,11 @@
package = AB6693C82E65C94400BCAAC1 /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKitSwift;
};
AB6695262E67015600BCAAC1 /* WaterfallGrid */ = {
isa = XCSwiftPackageProductDependency;
package = AB6695252E67015600BCAAC1 /* XCRemoteSwiftPackageReference "WaterfallGrid" */;
productName = WaterfallGrid;
};
ABC150C02E5DB39A00A1F970 /* Lottie */ = {
isa = XCSwiftPackageProductDependency;
package = ABC150BF2E5DB39A00A1F970 /* XCRemoteSwiftPackageReference "lottie-spm" */;

View File

@ -1,5 +1,5 @@
{
"originHash" : "d4b9379b4bd658fe79a6ae528c96d3386427dfe9d23635a65dad6edf12af85ff",
"originHash" : "7ea295cc5e3eb8ef644b89ce2b47a7600994b67c8582ee354b643cd63250740d",
"pins" : [
{
"identity" : "alamofire",
@ -45,6 +45,15 @@
"revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
"version" : "1.6.4"
}
},
{
"identity" : "waterfallgrid",
"kind" : "remoteSourceControl",
"location" : "https://github.com/paololeonardi/WaterfallGrid.git",
"state" : {
"revision" : "c7c08652c3540adf8e48409c351879b4caea7e89",
"version" : "1.1.0"
}
}
],
"version" : 3

View File

@ -1,5 +1,6 @@
import SwiftUI
import AVKit
import WaterfallGrid
// MARK: - API Response Models
struct MaterialResponse: Decodable {
@ -98,30 +99,43 @@ struct MemoriesView: View {
ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea()
Group {
if isLoading {
ProgressView()
.scaleEffect(1.5)
} else if let error = errorMessage {
Text("Error: \(error)")
.foregroundColor(.red)
} else {
// Group {
// if isLoading {
// ProgressView()
// .scaleEffect(1.5)
// } else if let error = errorMessage {
// Text("Error: \(error)")
// .foregroundColor(.red)
// } else {
// ScrollView {
// LazyVGrid(columns: columns, spacing: 4) {
// ForEach(memories) { memory in
// MemoryCard(memory: memory)
// .padding(.horizontal, 2)
// .onTapGesture {
// withAnimation(.spring()) {
// selectedMemory = memory
// }
// }
// }
// }
// .padding(.top, 4)
// .padding(.horizontal, 4)
// }
// }
// }
// Replace the WaterfallGrid line with this:
ScrollView {
LazyVGrid(columns: columns, spacing: 4) {
ForEach(memories) { memory in
WaterfallGrid(memories) { memory in
MemoryCard(memory: memory)
.padding(.horizontal, 2)
.onTapGesture {
withAnimation(.spring()) {
selectedMemory = memory
}
}
}
}
.padding(.top, 4)
.padding(.horizontal, 4)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
}
}
@ -372,20 +386,45 @@ struct VideoPlayer: UIViewRepresentable {
struct MemoryCard: View {
let memory: MemoryItem
@State private var aspectRatio: CGFloat = 1.0
@State private var isLoading = true
private func loadAspectRatio(from url: URL) {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
let width = imageProperties[kCGImagePropertyPixelWidth] as? CGFloat,
let height = imageProperties[kCGImagePropertyPixelHeight] as? CGFloat,
height > 0 else {
aspectRatio = 16/9 // Default to 16:9 if we can't determine the aspect ratio
isLoading = false
return
}
aspectRatio = width / height
isLoading = false
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
ZStack {
// Media content
Group {
switch memory.mediaType {
case .image(let url):
if let url = URL(string: url) {
AsyncImage(url: url) { phase in
Group {
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.onAppear {
// Get image dimensions
if let uiImage = image.asUIImage() {
let size = uiImage.size
aspectRatio = size.width / size.height
isLoading = false
}
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
@ -393,34 +432,38 @@ struct MemoryCard: View {
}
}
}
}
case .video(_, let previewUrl):
if let previewUrl = URL(string: previewUrl) {
AsyncImage(url: previewUrl) { phase in
Group {
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.onAppear {
loadAspectRatio(from: previewUrl)
}
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
ProgressView()
}
}
}
} else {
Color.gray.opacity(0.3)
}
}
}
.frame(width: (UIScreen.main.bounds.width / 2) - 24,
height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio))
.frame(
width: (UIScreen.main.bounds.width / 2) - 24,
height: (UIScreen.main.bounds.width / 2 - 24) / (isLoading ? 1 : aspectRatio)
)
.clipped()
.cornerRadius(12)
.onAppear {
print(" Memory Card Appeared - Media Type: \(memory.mediaType)")
}
// Show play button for videos
if case .video = memory.mediaType {
Image(systemName: "play.circle.fill")
.font(.system(size: 40))
@ -429,15 +472,11 @@ struct MemoryCard: View {
}
}
// Title and Subtitle
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
Text(memory.title)
.font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain)
.lineLimit(1)
.onTapGesture {
print("🐰🐰🐰🐰🐰🐰🐰🐰🐰🐰🐰 \(memory)")
}
Text(memory.subtitle)
.font(.system(size: 14))
@ -450,6 +489,23 @@ struct MemoryCard: View {
}
}
// Add this extension to get UIImage from Image
extension View {
func asUIImage() -> UIImage? {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
#Preview {
MemoriesView()
}