Skip to content

Instantly share code, notes, and snippets.

@fatbobman
Last active May 9, 2024 02:02
Show Gist options
  • Save fatbobman/dca984afab837a72c2da30a427f312ef to your computer and use it in GitHub Desktop.
Save fatbobman/dca984afab837a72c2da30a427f312ef to your computer and use it in GitHub Desktop.
A forked version of ContainerRelativeFrame
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)))
}
}
@fatbobman
Copy link
Author

  1. ContainerType 可以加上关联值,使用 container 时不用做类型转换;
  2. .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