126 lines
4.5 KiB
Swift
126 lines
4.5 KiB
Swift
|
//
|
||
|
// MatchedGeometryModifiers.swift
|
||
|
// MatchGeom
|
||
|
//
|
||
|
// Created by Shadowfacts on 4/24/23.
|
||
|
//
|
||
|
|
||
|
import SwiftUI
|
||
|
|
||
|
extension View {
|
||
|
public func matchedGeometryPresentation<ID: Hashable, Presented: View>(id: Binding<ID?>, backgroundColor: UIColor, @ViewBuilder presenting: () -> Presented) -> some View {
|
||
|
self.modifier(MatchedGeometryPresentationModifier(id: id, backgroundColor: backgroundColor, presented: presenting()))
|
||
|
}
|
||
|
|
||
|
public func matchedGeometrySource<ID: Hashable, ID2: Hashable>(id: ID, presentationID: ID2) -> some View {
|
||
|
self.modifier(MatchedGeometrySourceModifier(id: AnyHashable(id), presentationID: AnyHashable(presentationID), matched: { AnyView(self) }))
|
||
|
}
|
||
|
|
||
|
public func matchedGeometryDestination<ID: Hashable>(id: ID) -> some View {
|
||
|
self.modifier(MatchedGeometryDestinationModifier(id: AnyHashable(id), matched: self))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private struct MatchedGeometryPresentationModifier<ID: Hashable, Presented: View>: ViewModifier {
|
||
|
@Binding var id: ID?
|
||
|
let backgroundColor: UIColor
|
||
|
let presented: Presented
|
||
|
@StateObject private var state = MatchedGeometryState()
|
||
|
|
||
|
private var isPresented: Binding<Bool> {
|
||
|
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<Matched: View>: 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
|
||
|
}
|