I am basically trying to recreate the photos app. In doing so, matched geometry effect should be the best way to recreate the animation that is used in the photos app when you click on an image/close it. However, on opening of an image it only does half of the animation. When closing the image the animation is only contained to the lazyvgrid individual image not the whole view. Also the first image of the gallery simply does not animate when closing.
Gallery view is made from a lazyvgrid and for each, full screen view is made of a tabview and for each.
Here is what it looks like:
Main view:
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)
}
}
}
}
}
}
Gallery Image View:
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()
}
}
}
Image full screen view (tab view):
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))
}
}
}
This is how far I got. The zoom out from FullScreenView to GalleryView works. the only thing that doesn't, is a clean zoom in into the TabView. I suppose this is because of the wrapping by 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)
}
}