Last active
May 9, 2024 02:02
-
-
Save fatbobman/dca984afab837a72c2da30a427f312ef to your computer and use it in GitHub Desktop.
A forked version of ContainerRelativeFrame
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
import Foundation | |
import SwiftUI | |
import UIKit | |
extension UIView { | |
fileprivate func findRelevantContainer() -> ContainerType? { | |
var responder: UIResponder? = self | |
while let currentResponder = responder { | |
if let viewController = currentResponder as? UIViewController { | |
if let tabview = viewController as? UITabBarController { | |
return .tabview(tabview) // UITabBarController | |
} | |
if let navigator = viewController as? UINavigationController { | |
return .navigator(navigator) // UINavigationController | |
} | |
} | |
if let scrollView = currentResponder as? UIScrollView { | |
return .scrollView(scrollView) // UIScrollView | |
} | |
responder = currentResponder.next | |
} | |
if let currentWindow { | |
return .window(currentWindow) // UIWindow | |
} else { | |
return nil | |
} | |
} | |
} | |
private struct ContainerDetector: UIViewRepresentable { | |
@Binding var size: CGSize? | |
func makeCoordinator() -> Coordinator { | |
.init(size: _size) | |
} | |
init(size: Binding<CGSize?>) { | |
_size = size | |
} | |
func makeUIView(context _: Context) -> UIView { | |
let detector = UIView() | |
detector.backgroundColor = .clear | |
return detector | |
} | |
func updateUIView(_ uiview: UIView, context: Context) { | |
DispatchQueue.main.async { | |
guard context.coordinator.cancellable == nil else { return } | |
if let container = uiview.findRelevantContainer() { | |
context.coordinator.trackContainerSizeChanges(ofType: container) | |
} | |
} | |
} | |
@MainActor | |
class Coordinator: NSObject, ObservableObject { | |
var size: Binding<CGSize?> | |
var cancellable: AnyCancellable? | |
init(size: Binding<CGSize?>) { | |
self.size = size | |
} | |
func trackContainerSizeChanges(ofType type: ContainerType) { | |
switch type { | |
case let .window(window): | |
cancellable = window.publisher(for: \.frame) | |
.receive(on: RunLoop.main) | |
.sink(receiveValue: { [weak self] _ in | |
guard let self = self else { return } | |
let size = self.calculateContainerSize(ofType: type) | |
self.size.wrappedValue = size | |
}) | |
case let .navigator(navigator): | |
cancellable = navigator.view.publisher(for: \.frame) | |
.receive(on: RunLoop.main) | |
.sink { [weak self] _ in | |
guard let self = self else { return } | |
let size = self.calculateContainerSize(ofType: type) | |
self.size.wrappedValue = size | |
} | |
case let .scrollView(scrollView): // scrollView is UIScrollView | |
cancellable = scrollView.publisher(for: \.frame) | |
.receive(on: RunLoop.main) | |
.sink { [weak self] _ in | |
guard let self = self else { return } | |
let size = self.calculateContainerSize(ofType: type) | |
self.size.wrappedValue = size | |
} | |
case let .tabview(tabview): | |
cancellable = tabview.view.publisher(for: \.frame) | |
.receive(on: RunLoop.main) | |
.sink { [weak self] _ in | |
guard let self = self else { return } | |
let size = self.calculateContainerSize(ofType: type) | |
self.size.wrappedValue = size | |
} | |
} | |
} | |
func calculateContainerSize(ofType type: ContainerType) -> CGSize { | |
switch type { | |
case let .window(window): | |
let windowSize = window.frame.size | |
let safeAreaInsets = window.safeAreaInsets | |
let width = windowSize.width - safeAreaInsets.left - safeAreaInsets.right | |
let height = windowSize.height - safeAreaInsets.top - safeAreaInsets.bottom | |
return CGSize(width: width, height: height) | |
case let .navigator(navigator): | |
let navigatorSize = navigator.view.frame.size | |
let safeAreaInsets = navigator.view.safeAreaInsets | |
var navigationBarHeight: CGFloat = 0 | |
// 在 NavigationSplitView 中时,计算高度需要去除 barHeight | |
if navigator.parent is UISplitViewController { | |
navigationBarHeight = navigator.navigationBar.frame.height | |
} | |
let width = navigatorSize.width - safeAreaInsets.left - safeAreaInsets.right | |
let height = navigatorSize.height - safeAreaInsets.top - safeAreaInsets.bottom - navigationBarHeight | |
return CGSize(width: width, height: height) | |
case let .scrollView(scrollview): | |
let scrollviewSize = scrollview.frame.size | |
let safeAreaInsets = scrollview.safeAreaInsets | |
let width = scrollviewSize.width - safeAreaInsets.left - safeAreaInsets.right | |
let height = scrollviewSize.height - safeAreaInsets.top - safeAreaInsets.bottom | |
return CGSize(width: width, height: height) | |
case let .tabview(tabview): | |
let tabviewSize = tabview.view.frame.size | |
let barHeight = tabview.tabBar.frame.height | |
let safeAreaInsets = tabview.view.safeAreaInsets | |
let width = tabviewSize.width - safeAreaInsets.left - safeAreaInsets.right | |
// tabview 底部高度要去除 barHeight | |
let height = tabviewSize.height - safeAreaInsets.top - barHeight | |
return CGSize(width: width, height: height) | |
} | |
} | |
} | |
} | |
private enum ContainerType { | |
case scrollView(UIScrollView) | |
case navigator(UINavigationController) | |
case tabview(UITabBarController) | |
case window(UIWindow) | |
} | |
extension UIView { | |
// UIView 对应的 UIWindow | |
fileprivate var currentWindow: UIWindow? { | |
var parentResponder: UIResponder? = self | |
while let nextResponder = parentResponder?.next { | |
parentResponder = nextResponder | |
if let window = parentResponder as? UIWindow { | |
return window | |
} | |
} | |
return nil | |
} | |
} | |
private struct ContainerDetectorModifier: ViewModifier { | |
let type: DetectorType | |
@State private var containerSize: CGSize? | |
func body(content: Content) -> some View { | |
let sizeInfo = result | |
content | |
.background( | |
ContainerDetector(size: $containerSize) | |
) | |
.frame(width: sizeInfo.width, height: sizeInfo.height, alignment: sizeInfo.alignment) | |
} | |
var result: (width: CGFloat?, height: CGFloat?, alignment: Alignment) { | |
var width: CGFloat? | |
var height: CGFloat? | |
var align: Alignment = .center | |
switch type { | |
case let .standard(axes, alignment): | |
if axes.contains(.horizontal) { | |
width = containerSize?.width | |
} | |
if axes.contains(.vertical) { | |
height = containerSize?.height | |
} | |
align = alignment | |
case let .custom(axes, alignment, length): | |
if axes.contains(.horizontal), let w = containerSize?.width { | |
width = length(w, .horizontal) | |
} | |
if axes.contains(.vertical), let h = containerSize?.height { | |
height = length(h, .vertical) | |
} | |
align = alignment | |
case let .grid(axes, count, span, spacing, alignment): | |
if axes.contains(.horizontal), let w = containerSize?.width { | |
let availableWidth = (w - (spacing * CGFloat(count - 1))) | |
let columnWidth = (availableWidth / CGFloat(count)) | |
width = (columnWidth * CGFloat(span)) + (CGFloat(span - 1) * spacing) | |
} | |
if axes.contains(.vertical), let h = containerSize?.height { | |
let availableHeight = (h - (spacing * CGFloat(count - 1))) | |
let rowHeight = (availableHeight / CGFloat(count)) | |
height = (rowHeight * CGFloat(span)) + (CGFloat(span - 1) * spacing) | |
} | |
align = alignment | |
} | |
if width ?? 0 < 0 { width = nil } | |
if height ?? 0 < 0 { width = nil } | |
return (width, height, align) | |
} | |
enum DetectorType { | |
case standard(axes: Axis.Set, alignment: Alignment) | |
case grid(axes: Axis.Set, count: Int, span: Int, spacing: CGFloat, alignment: Alignment) | |
case custom(axes: Axis.Set, alignment: Alignment, length: (CGFloat, Axis) -> CGFloat) | |
} | |
} | |
extension View { | |
@available(iOS 13.0, *) | |
public func myContainerRelativeFrame(_ axes: Axis.Set, alignment: Alignment = .center) -> some View { | |
modifier(ContainerDetectorModifier(type: .standard(axes: axes, alignment: alignment))) | |
} | |
@available(iOS 13.0,*) | |
public func myContainerRelativeFrame(_ axes: Axis.Set, count: Int, span: Int = 1, spacing: CGFloat, alignment: Alignment = .center) -> some View { | |
modifier(ContainerDetectorModifier(type: .grid(axes: axes, count: count, span: span, spacing: spacing, alignment: alignment))) | |
} | |
@available(iOS 13.0,*) | |
public func myContainerRelativeFrame(_ axes: Axis.Set, alignment: Alignment = .center, _ length: @escaping (CGFloat, Axis) -> CGFloat) -> some View { | |
modifier(ContainerDetectorModifier(type: .custom(axes: axes, alignment: alignment, length: length))) | |
} | |
} |
ContainerType
可以加上关联值,使用container
时不用做类型转换;.frame(width: result.width, height: result.height, alignment: result.alignment)
这里感觉会计算 3 次result
,用@State
保存会好一点?
感谢指正。代码写的比较匆忙,主要为了对猜想做验证。代码已经做了调整
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ContainerType
可以加上关联值,使用container
时不用做类型转换;.frame(width: result.width, height: result.height, alignment: result.alignment)
这里感觉会计算 3 次result
,用@State
保存会好一点?