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

View File

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

View File

@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import AVKit import AVKit
import WaterfallGrid
// MARK: - API Response Models // MARK: - API Response Models
struct MaterialResponse: Decodable { struct MaterialResponse: Decodable {
@ -98,30 +99,43 @@ struct MemoriesView: View {
ZStack { ZStack {
Color.themeTextWhiteSecondary.ignoresSafeArea() Color.themeTextWhiteSecondary.ignoresSafeArea()
Group { // Group {
if isLoading { // if isLoading {
ProgressView() // ProgressView()
.scaleEffect(1.5) // .scaleEffect(1.5)
} else if let error = errorMessage { // } else if let error = errorMessage {
Text("Error: \(error)") // Text("Error: \(error)")
.foregroundColor(.red) // .foregroundColor(.red)
} else { // } else {
ScrollView { // ScrollView {
LazyVGrid(columns: columns, spacing: 4) { // LazyVGrid(columns: columns, spacing: 4) {
ForEach(memories) { memory in // ForEach(memories) { memory in
MemoryCard(memory: memory) // MemoryCard(memory: memory)
.padding(.horizontal, 2) // .padding(.horizontal, 2)
.onTapGesture { // .onTapGesture {
withAnimation(.spring()) { // withAnimation(.spring()) {
selectedMemory = memory // selectedMemory = memory
} // }
} // }
// }
// }
// .padding(.top, 4)
// .padding(.horizontal, 4)
// }
// }
// }
// Replace the WaterfallGrid line with this:
ScrollView {
WaterfallGrid(memories) { memory in
MemoryCard(memory: memory)
.onTapGesture {
withAnimation(.spring()) {
selectedMemory = memory
} }
} }
.padding(.top, 4)
.padding(.horizontal, 4)
}
} }
.padding(.horizontal, 8)
.padding(.vertical, 4)
} }
} }
} }
@ -372,24 +386,50 @@ struct VideoPlayer: UIViewRepresentable {
struct MemoryCard: View { struct MemoryCard: View {
let memory: MemoryItem 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 { var body: some View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) {
ZStack { ZStack {
// Media content
Group { Group {
switch memory.mediaType { switch memory.mediaType {
case .image(let url): case .image(let url):
if let url = URL(string: url) { if let url = URL(string: url) {
AsyncImage(url: url) { phase in AsyncImage(url: url) { phase in
if let image = phase.image { Group {
image if let image = phase.image {
.resizable() image
.aspectRatio(contentMode: .fill) .resizable()
} else if phase.error != nil { .aspectRatio(contentMode: .fill)
Color.gray.opacity(0.3) .onAppear {
} else { // Get image dimensions
ProgressView() 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 {
ProgressView()
}
} }
} }
} }
@ -397,14 +437,19 @@ struct MemoryCard: View {
case .video(_, let previewUrl): case .video(_, let previewUrl):
if let previewUrl = URL(string: previewUrl) { if let previewUrl = URL(string: previewUrl) {
AsyncImage(url: previewUrl) { phase in AsyncImage(url: previewUrl) { phase in
if let image = phase.image { Group {
image if let image = phase.image {
.resizable() image
.aspectRatio(contentMode: .fill) .resizable()
} else if phase.error != nil { .aspectRatio(contentMode: .fill)
Color.gray.opacity(0.3) .onAppear {
} else { loadAspectRatio(from: previewUrl)
ProgressView() }
} else if phase.error != nil {
Color.gray.opacity(0.3)
} else {
ProgressView()
}
} }
} }
} else { } else {
@ -412,15 +457,13 @@ struct MemoryCard: View {
} }
} }
} }
.frame(width: (UIScreen.main.bounds.width / 2) - 24, .frame(
height: (UIScreen.main.bounds.width / 2 - 24) * (1/memory.aspectRatio)) width: (UIScreen.main.bounds.width / 2) - 24,
height: (UIScreen.main.bounds.width / 2 - 24) / (isLoading ? 1 : aspectRatio)
)
.clipped() .clipped()
.cornerRadius(12) .cornerRadius(12)
.onAppear {
print(" Memory Card Appeared - Media Type: \(memory.mediaType)")
}
// Show play button for videos
if case .video = memory.mediaType { if case .video = memory.mediaType {
Image(systemName: "play.circle.fill") Image(systemName: "play.circle.fill")
.font(.system(size: 40)) .font(.system(size: 40))
@ -429,15 +472,11 @@ struct MemoryCard: View {
} }
} }
// Title and Subtitle VStack(alignment: .leading, spacing: 4) {
VStack(alignment: .leading, spacing: 16) {
Text(memory.title) Text(memory.title)
.font(Typography.font(for: .body, family: .quicksandBold)) .font(Typography.font(for: .body, family: .quicksandBold))
.foregroundColor(.themeTextMessageMain) .foregroundColor(.themeTextMessageMain)
.lineLimit(1) .lineLimit(1)
.onTapGesture {
print("🐰🐰🐰🐰🐰🐰🐰🐰🐰🐰🐰 \(memory)")
}
Text(memory.subtitle) Text(memory.subtitle)
.font(.system(size: 14)) .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 { #Preview {
MemoriesView() MemoriesView()
} }