// // MatchedGeometryModifiers.swift // MatchGeom // // Created by Shadowfacts on 4/24/23. // import SwiftUI extension View { public func matchedGeometryPresentation(id: Binding, backgroundColor: UIColor, @ViewBuilder presenting: () -> Presented) -> some View { self.modifier(MatchedGeometryPresentationModifier(id: id, backgroundColor: backgroundColor, presented: presenting())) } public func matchedGeometrySource(id: ID, presentationID: ID2) -> some View { self.modifier(MatchedGeometrySourceModifier(id: AnyHashable(id), presentationID: AnyHashable(presentationID), matched: { AnyView(self) })) } public func matchedGeometryDestination(id: ID) -> some View { self.modifier(MatchedGeometryDestinationModifier(id: AnyHashable(id), matched: self)) } } private struct MatchedGeometryPresentationModifier: ViewModifier { @Binding var id: ID? let backgroundColor: UIColor let presented: Presented @StateObject private var state = MatchedGeometryState() private var isPresented: Binding { Binding { id != nil } set: { if $0 { fatalError() } else { id = nil } } } func body(content: Content) -> some View { content .environmentObject(state) .backgroundPreferenceValue(MatchedGeometrySourcesKey.self, { sources in Color.clear .presentViewController(makeVC(allSources: sources), isPresented: isPresented) }) } private func makeVC(allSources: [SourceKey: (AnyView, CGRect)]) -> () -> UIViewController { return { // force unwrap is safe, this closure is only called when being presented so we must have an id let id = AnyHashable(id!) return MatchedGeometryViewController( presentationID: id, content: presented, state: state, backgroundColor: backgroundColor ) } } } private struct MatchedGeometrySourceModifier: ViewModifier { let id: AnyHashable let presentationID: AnyHashable let matched: () -> AnyView @EnvironmentObject private var state: MatchedGeometryState func body(content: Content) -> some View { content .background(GeometryReader { proxy in Color.clear .preference(key: MatchedGeometryDestinationFrameKey.self, value: proxy.frame(in: .global)) .onPreferenceChange(MatchedGeometryDestinationFrameKey.self) { newValue in if let newValue { state.sources[SourceKey(presentationID: presentationID, matchedID: id)] = (matched, newValue) } } }) .opacity(state.animating && state.presentationID == presentationID ? 0 : 1) } } private struct MatchedGeometryDestinationModifier: ViewModifier { let id: AnyHashable let matched: Matched @EnvironmentObject private var state: MatchedGeometryState func body(content: Content) -> some View { content .background(GeometryReader { proxy in Color.clear .preference(key: MatchedGeometryDestinationFrameKey.self, value: proxy.frame(in: .global)) .onPreferenceChange(MatchedGeometryDestinationFrameKey.self) { newValue in if let newValue, // ignore intermediate layouts that may happen while the dismiss animation is happening state.mode != .dismissing { state.destinations[id] = (AnyView(matched), newValue) } } }) .opacity(state.animating ? 0 : 1) } } private struct MatchedGeometryDestinationFrameKey: PreferenceKey { static let defaultValue: CGRect? = nil static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) { value = nextValue() } } private struct MatchedGeometrySourcesKey: PreferenceKey { static let defaultValue: [SourceKey: (AnyView, CGRect)] = [:] static func reduce(value: inout Value, nextValue: () -> Value) { value.merge(nextValue(), uniquingKeysWith: { _, new in new }) } } struct SourceKey: Hashable { let presentationID: AnyHashable let matchedID: AnyHashable }