SwiftUI 匹配几何效果不适用于多个 ForEach

SwiftUI Matched Geometry Effect not working with multiple ForEach's

我基本上是在尝试重新创建照片应用程序。在这样做时,匹配的几何效果应该是在您单击 image/close 照片应用程序时重新创建照片应用程序中使用的动画的最佳方式。但是,在打开图像时它只执行一半的动画。关闭图像时,动画仅包含在 lazyvgrid 单个图像中,而不是整个视图。此外,画廊的第一张图片在关闭时根本没有动画。

图库视图由 lazyvgrid 和每个视图组成,全屏视图由 tabview 和每个视图组成。

这是它的样子:

主视图:

struct ImageSelectorView: View {
    @EnvironmentObject var isvm: ImageSelectorViewModel
    @Namespace var namespace
    @State private var selectedImages: [SelectedImagesModel] = []
    @State private var selectedImageID: String = ""
    @State private var liveEventID: String = ""
    @State var showImageFSV: Bool = false
    @State var showPicker: Bool = false
    @Binding var liveEvent: [EventModel]
    public var pickerConfig: PHPickerConfiguration {
        var config = PHPickerConfiguration(photoLibrary: .shared())
        config.filter = .any(of: [.images, .livePhotos, .videos])
        config.selectionLimit = 10
        return config
    }
    private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
    private let viewWidth: CGFloat = UIScreen.main.bounds.width
    private let viewHeight: CGFloat = UIScreen.main.bounds.height
    private let viewHPadding: CGFloat = 30
    
    init(liveEvent: Binding<[EventModel]>) {
        self._liveEvent = liveEvent
    }
    
    var body: some View {
        ZStack {
            Color.theme.background.ignoresSafeArea()
            VStack {
                ZStack {
                    ScrollView(.vertical, showsIndicators: true) {
                        LazyVGrid(columns: self.gridItemLayout, alignment: .center, spacing: 0.5) {
                            ForEach(self.liveEvent[0].eventImages.indices) { image in
                                GalleryImage(selectedImageID: self.$selectedImageID, showImageFSV: self.$showImageFSV, image: self.liveEvent[0].eventImages[image], namespace: self.namespace)
                            }
                        }
                    }
                    
                    if self.showImageFSV {
                        KFImagesFSV(eventImages: self.$liveEvent[0].eventImages, showImageFSV: self.$showImageFSV, selectedImageID: self.$selectedImageID, namespace: self.namespace)
                    }
                }
            }
        }
    }
}

图库图片视图:

struct GalleryImage: View {
    @Binding var selectedImageID: String
    @Binding var showImageFSV: Bool
    public var image: EventImage
    public var namespace: Namespace.ID
    private let viewWidth: CGFloat = UIScreen.main.bounds.width
    private let viewHeight: CGFloat = UIScreen.main.bounds.height
    var body: some View {
        Button {
            DispatchQueue.main.async {
                withAnimation(.spring()) {
                    self.selectedImageID = image.id
                    if self.selectedImageID == image.id {
                        self.showImageFSV.toggle()
                    }
                }
            }
        } label: {
            KFImage(URL(string: image.url))
                .placeholder({
                    Image("topo")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                })
                .loadDiskFileSynchronously()
                .cacheMemoryOnly()
                .fade(duration: 0.2)
                .resizable()
                .matchedGeometryEffect(id: self.selectedImageID == image.id ? "" : image.id, in: self.namespace)
                .aspectRatio(contentMode: .fill)
                .frame(width: (self.viewWidth/2.9) - 3, height: (self.viewWidth/2.9) - 3)
                .clipped()
        }
    }
}

图片全屏视图(标签视图):

struct KFImagesFSV: View {
    @Binding var eventImages: [EventImage]
    @Binding var showImageFSV: Bool
    @Binding var selectedImageID: String
    public var namespace: Namespace.ID
    private let viewWidth: CGFloat = UIScreen.main.bounds.width
    private let viewHeight: CGFloat = UIScreen.main.bounds.height
    private let viewHPadding: CGFloat = 30
    var body: some View {
        ZStack {
            TabView(selection: self.$selectedImageID) {
                ForEach(self.eventImages.indices) { image in
                    KFImage(URL(string: self.eventImages[image].url))
                        .placeholder({
                            Image("topo")
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                        })
                        .loadDiskFileSynchronously()
                        .cacheMemoryOnly()
                        .fade(duration: 0.2)
                        .resizable()
                        .tag(self.eventImages[image].id)
                        .matchedGeometryEffect(id: self.selectedImageID == self.eventImages[image].id ? self.eventImages[image].id : "", in: self.namespace)
                        .aspectRatio(contentMode: .fit)
                        .frame(width: self.viewWidth, height: self.viewHeight)
                }
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
        }
    }
}

这就是我的进展。从 FullScreenView 缩小到 GalleryView 有效。唯一没有的是 TabView 的干净放大。我想这是因为 TabView 的包装。

struct ImageStruct: Identifiable {
    let id = UUID()
    var image: String = ""
}

let imagesArray = [
    ImageStruct(image: "image1"),
    ImageStruct(image: "image2"),
    ImageStruct(image: "image3"),
    ImageStruct(image: "image4"),
    ImageStruct(image: "image5"),
    ImageStruct(image: "image6"),
    ImageStruct(image: "image7"),
    ImageStruct(image: "image8")
]



struct ContentView: View {
    
    @Namespace var ns
    @State private var selectedImage: UUID?
    
    var body: some View {
//        ZStack {
            if selectedImage == nil {
                GalleryView(selectedImage: $selectedImage, ns: ns)
            } else {
                FullScreenView(selectedImage: $selectedImage, ns: ns)
            }
//        }
    }
}


struct GalleryView: View {
    
    @Binding var selectedImage: UUID?
    var ns: Namespace.ID
    
    private let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
    
    var body: some View {
        VStack {
            ScrollView(.vertical, showsIndicators: true) {
                
                LazyVGrid(columns: columns) {
                    
                    ForEach(imagesArray) { image in
                        
                        Color.clear.overlay(
                            Image(image.image)
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                                .matchedGeometryEffect(id: image.id, in: ns, isSource: true)
                        )
                            .clipped()
                            .aspectRatio(1, contentMode: .fit)
                        
                            .onTapGesture {
                                withAnimation {
                                    selectedImage = image.id
                                }
                            }
                    }
                }
            }
        }
    }
}


struct FullScreenView: View {
    
    @Binding var selectedImage: UUID?
    var ns: Namespace.ID
    
    init(selectedImage: Binding<UUID?>, ns: Namespace.ID) {
        print(selectedImage)
        self._selectedImage = selectedImage
        self.ns = ns
        // initialize selctedTab to selectedImage
        self._selectedTab = State(initialValue: selectedImage.wrappedValue ?? UUID())
    }
    
    
    @State private var selectedTab: UUID
    
    var body: some View {
        
        TabView(selection: $selectedTab) {
            
            ForEach(imagesArray) { image in
                
                Image(image.image)
                    .resizable()
                    .scaledToFit()
                    // ternary applying effect only for selected tab
                    .matchedGeometryEffect(id: image.id == selectedTab ? selectedTab : UUID(),
                                           in: ns, isSource: true)
                
                    .tag(image.id)
                
                    .onTapGesture {
                        withAnimation {
                            selectedImage = nil
                        }
                    }
            }
        }
        .tabViewStyle(.page)
    }
}

按照 ChrisR 的回答,我决定添加一个允许滑动关闭的手势。这对于 TabView 是不可能的,所以我用 LazyHstack 重新制作了整个东西。我不会将此标记为正确答案,因为原始问题不包含 lazyhstack。这只是解决原始问题的另一种方法。

Link 简化代码:

github repo

图库视图:

struct EventGalleryView: View {
    @Namespace var namespace
    @GestureState private var selectedImageOffset: CGSize = .zero
    @Binding var eventImages: [EventImage]
    @State private var selectedImageIndex: Int? = nil
    @State private var selectedImageScale: CGFloat = 1
    @State private var showFSV: Bool = false
    @State private var isSwiping: Bool = false
    @State private var isSelecting: Bool = false
    private var gridItemLayout = Array(repeating: GridItem(.flexible()), count: 3)
    init(eventImages: Binding<[EventImage]>) {
        self._eventImages = eventImages
    }
    var body: some View {
        GeometryReader { geo in
            let geoWidth = geo.size.width
            let geoHeight = geo.size.height
            ScrollView(.vertical, showsIndicators: true) {
                LazyVGrid(columns: self.gridItemLayout, alignment: .center, spacing: 0.5) {
                    ForEach(eventImages) { image in
                        GalleryImageView(image: image)
                            .matchedGeometryEffect(id: eventImages.firstIndex(of: image), in: self.namespace, isSource: self.showFSV ? false : true)
                            .aspectRatio(contentMode: .fill)
                            .frame(width: (geoWidth/2.9) - 3, height: (geoWidth/2.9) - 3, alignment: .center)
                            .clipped()
                            .contentShape(Rectangle())
                            .opacity(eventImages.firstIndex(of: image) == selectedImageIndex ? 0 : 1)
                            .onTapGesture {
                                DispatchQueue.main.async {
                                    withAnimation(.spring()) {
                                        self.showFSV = true
                                        self.selectedImageIndex = eventImages.firstIndex(of: image)
                                    }
                                }
                            }
                    }
                }
            }
            .zIndex(0)
            
            ImageFSV(selectedImageOffset: self.selectedImageOffset, showFSV: self.$showFSV, selectedImageIndex: self.$selectedImageIndex, selectedImageScale: self.$selectedImageScale, isSelecting: self.$isSelecting, isSwiping: self.$isSwiping, eventImages: self.eventImages, geoWidth: geoWidth, geoHeight: geoHeight, namespace: self.namespace)
        }
    }
}

FSV 图像视图:

struct ImageFSV: View {
    @GestureState var selectedImageOffset: CGSize
    @State private var backgroundOpacity: CGFloat = 1
    @Binding var showFSV: Bool
    @Binding var selectedImageIndex: Int?
    @Binding var selectedImageScale: CGFloat
    @Binding var isSelecting: Bool
    @Binding var isSwiping: Bool
    public var eventImages: [EventImage]
    public let geoWidth: CGFloat
    public let geoHeight: CGFloat
    public let namespace: Namespace.ID
    var body: some View {
        if self.showFSV, let index = self.selectedImageIndex {
            Color.theme.background.ignoresSafeArea()
                .opacity(self.backgroundOpacity)
                .zIndex(1)
            LazyHStack(spacing: 0) {
                ForEach(eventImages) { image in
                    GalleryImageView(image: image)
                        .if(self.eventImages.firstIndex(of: image) == self.selectedImageIndex && self.isSelecting, transform: { view in
                            view
                                .matchedGeometryEffect(id: self.selectedImageIndex, in: self.namespace, isSource: true)
                        })
                            .aspectRatio(contentMode: .fit)
                            .frame(width: geoWidth, height: geoHeight, alignment: .center)
                            .scaleEffect(eventImages.firstIndex(of: image) == self.selectedImageIndex ? self.selectedImageScale : 1)
                            .offset(x: -CGFloat(index) * geoWidth)
                            .offset(eventImages.firstIndex(of: image) == self.selectedImageIndex ? self.selectedImageOffset : .zero)
                }
            }
            .animation(.easeOut(duration: 0.25), value: index)
            .highPriorityGesture(
                DragGesture()
                    .onChanged({ value in
                        DispatchQueue.main.async {
                            if !self.isSelecting && (value.translation.width > 5 || value.translation.width < -5) {
                                self.isSwiping = true
                            }
                            if !self.isSwiping && (value.translation.height > 5 || value.translation.height < -5) {
                                self.isSelecting = true
                            }
                        }
                    })
                    .updating(self.$selectedImageOffset, body: { value, state, _ in
                        if self.isSwiping {
                            state = CGSize(width: value.translation.width, height: 0)
                        } else if self.isSelecting {
                            state = CGSize(width: value.translation.width, height: value.translation.height)
                        }
                    })
                    .onEnded({ value in
                        DispatchQueue.main.async {
                            self.isSwiping = false
                            if value.translation.height > 150 && self.isSelecting {
                                withAnimation(.spring()) {
                                    self.showFSV = false
                                    self.selectedImageIndex = nil
                                    self.isSelecting = false
                                }
                            } else {
                                self.isSelecting = false
                                let offset = value.translation.width / geoWidth*6
                                if offset > 0.5 && self.selectedImageIndex ?? 0 > 0 {
                                    self.selectedImageIndex! -= 1
                                } else if offset < -0.5 && self.selectedImageIndex ?? 0 < (eventImages.count - 1) {
                                    self.selectedImageIndex! += 1
                                }
                            }
                        }
                    })
            )
            .onChange(of: self.selectedImageOffset) { imageOffset in
                DispatchQueue.main.async {
                    withAnimation(.easeIn) {
                        switch imageOffset.height {
                            case 50..<70:
                                self.backgroundOpacity = 0.8
                            case 70..<90:
                                self.backgroundOpacity = 0.6
                            case 90..<110:
                                self.backgroundOpacity = 0.4
                            case 110..<130:
                                self.backgroundOpacity = 0.2
                            case 130..<1000:
                                self.backgroundOpacity = 0.0
                            default:
                                self.backgroundOpacity = 1.0
                        }
                    }
                    
                    let progress = imageOffset.height / geoHeight
                    if 1 - progress > 0.5 {
                        self.selectedImageScale = 1 - progress
                    }
                }
            }
            .zIndex(2)
        }
    }
}

画廊图像视图:

struct GalleryImageView: View {
    public let image: EventImage
    var body: some View {
        KFImage(URL(string: image.url))
            .placeholder({
                Image("topo")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            })
            .loadDiskFileSynchronously()
            .cacheMemoryOnly()
            .fade(duration: 0.2)
            .resizable()
    }
}

EventImage 模型:

struct EventImage: Identifiable, Codable, Hashable {
    var id = UUID().uuidString
    var url: String
    var voteCount: Int
    
    private enum eventImage: String, CodingKey {
        case id
        case imageURL
        case voteCount
    }
}

我正在建立一个类似的画廊,我能够让它发挥作用。 您需要在同一视图中包含 LazyVGrid 和 TabView。

然后像这样应用 matchedGeometryEffect:

HStack {
    TabView(...) {
        ...
    }
}.matchedGeometryEffect(...)

编辑:刚刚测试过 - 您可以将它们放在单独的视图中,但您需要在带有 TabView 的视图上应用 matchedGeometryEffect,而不是在视图内部。