Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
275751F62DEE1456003E467C /* OpenSwiftUIUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = OpenSwiftUIUITests; sourceTree = "<group>"; };
279FEC572DF450D200320390 /* ReferenceImages */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ReferenceImages; sourceTree = "<group>"; };
27E6C4F62D2842D80010502F /* Configurations */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); name = Configurations; path = ../Configurations; sourceTree = "<group>"; };
27FFF0422E08850C0060A4DA /* SharedExample */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SharedExample; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -179,6 +180,7 @@
27E6C4D02D2842530010502F /* OpenBox */,
271D81642BB1E8E300A6D543 /* OpenGraph */,
27CD0B612AFC8E0E003665EB /* OpenSwiftUI */,
27FFF0422E08850C0060A4DA /* SharedExample */,
275751AF2DEE136A003E467C /* Example */,
275751C12DEE136C003E467C /* HostingExample */,
275751E42DEE1441003E467C /* TestingHost */,
Expand Down Expand Up @@ -277,6 +279,7 @@
);
fileSystemSynchronizedGroups = (
275751AF2DEE136A003E467C /* Example */,
27FFF0422E08850C0060A4DA /* SharedExample */,
);
name = Example;
packageProductDependencies = (
Expand All @@ -300,6 +303,7 @@
);
fileSystemSynchronizedGroups = (
275751C12DEE136C003E467C /* HostingExample */,
27FFF0422E08850C0060A4DA /* SharedExample */,
);
name = HostingExample;
packageProductDependencies = (
Expand Down
17 changes: 4 additions & 13 deletions Example/HostingExample/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,9 @@ class ViewController: NSViewController {

struct ContentView: View {
var body: some View {
ZStack(alignment: .leading) {
Color.red
.opacity(0.5)
.frame(width: 200, height: 200)
}
.overlay(alignment: .topLeading) {
Color.green.opacity(0.5)
.frame(width: 100, height: 100)
}
.background(alignment: .bottomTrailing) {
Color.blue.opacity(0.5)
.frame(width: 100, height: 100)
}
MyViewThatFitsByLayout {
Color.red.frame(width: 50, height: 50)
Color.green.frame(width: 50, height: 50)
}.frame(width: 200, height: 200)
}
}
84 changes: 84 additions & 0 deletions Example/SharedExample/Layout/MyViewThatFitsByLayout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// MyViewThatFitsByLayout.swift
// SharedExample
//
// Modified from https://github.com/fatbobman/BlogCodes/blob/main/ViewThatFits/ViewThatFits/MyViewThatFitsByLayout.swift
// Copyright © 2022 Yang Xu. All rights reserved.

import Foundation
#if OPENSWIFTUI
import OpenSwiftUI
#else
import SwiftUI
#endif

struct _MyViewThatFitsLayout: Layout {
let axis: Axis.Set
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Int?) -> CGSize {
// No subviews, return zero
guard !subviews.isEmpty else { return .zero }
// One subview, returns the required size of the subview
guard subviews.count > 1 else {
cache = subviews.endIndex - 1
return subviews[subviews.endIndex - 1].sizeThatFits(proposal)
}
// From the first to the penultimate subview, obtain its ideal size in the limited axis one by one for judgment.
for i in 0..<subviews.count - 1 {
let size = subviews[i].dimensions(in: .unspecified)
switch axis {
case [.horizontal, .vertical]:
if size.width <= proposal.replacingUnspecifiedDimensions().width && size.height <= proposal.replacingUnspecifiedDimensions().height {
cache = i
// If the judgment conditions are met, return the required size of the subview (ask with the normal recommended size)
return subviews[i].sizeThatFits(proposal)
}
case .horizontal:
if size.width <= proposal.replacingUnspecifiedDimensions().width {
cache = i
return subviews[i].sizeThatFits(proposal)
}
case .vertical:
if size.height <= proposal.replacingUnspecifiedDimensions().height {
cache = i
return subviews[i].sizeThatFits(proposal)
}
default:
break
}
}
// If none of the above are satisfied, use the last subview
cache = subviews.endIndex - 1
return subviews[subviews.endIndex - 1].sizeThatFits(proposal)
}

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Int?) {
for i in subviews.indices {
if let cache, i == cache {
subviews[i].place(at: bounds.origin, anchor: .topLeading, proposal: proposal)
} else {
// Place the subviews that do not need to be displayed in a position that cannot be displayed
subviews[i].place(at: .init(x: 100_000, y: 100_000), anchor: .topLeading, proposal: .zero)
}
}
}

func makeCache(subviews _: Subviews) -> Int? {
nil
}
}

public struct MyViewThatFitsByLayout<Content>: View where Content: View {
let axis: Axis.Set
let content: Content

public init(axis: Axis.Set = [.horizontal, .vertical], @ViewBuilder content: @escaping () -> Content) {
self.axis = axis
self.content = content()
}

public var body: some View {
_MyViewThatFitsLayout(axis: axis) {
content
}
}
}
156 changes: 156 additions & 0 deletions Example/SharedExample/Layout/MyZStackLayout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//
// MyZStackLayout.swift
// SharedExample
//
// Modified from https://github.com/fatbobman/BlogCodes/blob/main/MyZStack/MyZStack/_MyZStackLayout.swift
// Copyright © 2022 Yang Xu. All rights reserved.

import Foundation
#if OPENSWIFTUI
import OpenSwiftUI
#else
import SwiftUI
#endif

private struct _MyZStackLayout: Layout {
let alignment: Alignment

func makeCache(subviews: Subviews) -> CacheInfo {
.init()
}

// 容器的父视图(父容器)将通过调用容器的 sizeThatFits 获取容器的需求尺寸,本方法通常会被多次调用,并提供不同的建议尺寸
func sizeThatFits(
proposal: ProposedViewSize, // 容器的父视图(父容器)提供的建议尺寸
subviews: Subviews, // 当前容器内的所有子视图的代理
cache: inout CacheInfo // 缓存数据,本例中用于保存子视图的返回的需求尺寸,减少调用次数
) -> CGSize {
cache = .init() // 清除缓存
for subview in subviews {
// 为子视图提供建议尺寸,获取子视图的需求尺寸 (ViewDimensions)
let viewDimension = subview.dimensions(in: proposal)
// 根据 MyZStack 的 alignment 的设置获取子视图的 alignmentGuide
let alignmentGuide: CGPoint = .init(
x: viewDimension[alignment.horizontal],
y: viewDimension[alignment.vertical]
)
// 以子视图的 alignmentGuide 为 (0,0) , 在虚拟的画布中,为子视图创建 CGRect
let bounds: CGRect = .init(
origin: .init(x: -alignmentGuide.x, y: -alignmentGuide.y),
size: .init(width: viewDimension.width, height: viewDimension.height)
)
// 保存子视图在虚拟画布中的数据
cache.subviewInfo.append(.init(viewDimension: viewDimension, bounds: bounds))
}

// 根据所有子视图在虚拟画布中的数据,生成 MyZtack 的 CGRect
cache.cropBounds = cache.subviewInfo.map(\.bounds).cropBounds()
// 返回当前容器的理想尺寸, 当前容器的父视图将使用该尺寸在它的内部进行摆放
return cache.cropBounds.size
}

// 容器的父视图(父容器)将在需要的时机调用本方法,为本容器的子视图设置渲染位置
func placeSubviews(
in bounds: CGRect, // 根据当前容器在 sizeThatFits 提供的尺寸,在真实渲染处创建的 CGRect
proposal: ProposedViewSize, // 容器的父视图(父容器)提供的建议尺寸
subviews: Subviews, // 当前容器内的所有子视图的代理
cache: inout CacheInfo // 缓存数据,本例中用于保存子视图的返回的需求尺寸,减少调用次数
) {
// 虚拟画布左上角的偏移值 ( 到 0,0 )
let offsetX = cache.cropBounds.minX * -1
let offsetY = cache.cropBounds.minY * -1

for index in subviews.indices {
let info = cache.subviewInfo[index]
// 将虚拟画布中的位置信息转换成渲染 bounds 的位置信息
let x = transformPoint(original: info.bounds.minX, offset: offsetX, targetBoundsMinX: bounds.minX)
let y = transformPoint(original: info.bounds.minY, offset: offsetY, targetBoundsMinX: bounds.minY)
// 将转换后的位置信息设置到子视图上,并为子视图设置渲染尺寸
subviews[index].place(at: .init(x: x, y: y), anchor: .topLeading, proposal: proposal)
}
}

// SwiftUI 通过此方法来获取特定的对齐参考的显式值
func explicitAlignment(
of guide: VerticalAlignment, // 查询的对齐指导
in bounds: CGRect, // 自定义容器的 bounds ,该 bounds 的尺寸由 sizeThatFits 方法计算得出,与 placeSubviews 的 bounds 参数一致
proposal: ProposedViewSize, // 父视图的推荐尺寸
subviews: Subviews, // 容器内的子视图代理
cache: inout CacheInfo // 缓存数据,本例中,我们在缓存数据中保存了每个子视图的 viewDimension、虚拟 bounds 能信息
) -> CGFloat? {
let offsetY = cache.cropBounds.minY * -1
let infinity: CGFloat = .infinity

// 检查子视图中是否有 显式 firstTextBaseline 不为 nil 的视图。如果有,则返回位置最高的 firstTextBaseline 值。
if guide == .firstTextBaseline,!cache.subviewInfo.isEmpty {
let firstTextBaseline = cache.subviewInfo.reduce(infinity) { current, info in
let baseline = info.viewDimension[explicit: .firstTextBaseline] ?? infinity
// 将子视图的显式 firstTextBaseline 转换成 bounds 中的偏移值
let transformBaseline = transformPoint(original: baseline + info.bounds.minY, offset: offsetY, targetBoundsMinX: 0)
// 返回位置最高的值( 值最小 )
return min(current, transformBaseline)
}
return firstTextBaseline != infinity ? firstTextBaseline : nil
}

if guide == .lastTextBaseline,!cache.subviewInfo.isEmpty {
let lastTextBaseline = cache.subviewInfo.reduce(-infinity) { current, info in
let baseline = info.viewDimension[explicit: .lastTextBaseline] ?? -infinity
let transformBaseline = transformPoint(original: baseline + info.bounds.minY, offset: offsetY, targetBoundsMinX: 0)
return max(current, transformBaseline)
}
return lastTextBaseline != -infinity ? lastTextBaseline : nil
}

return nil
}

func transformPoint(original: CGFloat, offset: CGFloat, targetBoundsMinX: CGFloat) -> CGFloat {
original + offset + targetBoundsMinX
}
}

extension _MyZStackLayout {
struct CacheInfo {
var subviewInfo: [SubViewInfo] = []
var cropBounds: CGRect = .zero
}

struct SubViewInfo {
let viewDimension: ViewDimensions
var bounds: CGRect = .zero
}
}

private extension Array where Element == CGRect {
func cropBounds() -> CGRect {
let leading = self.reduce(0) { currentLeading, bounds in
Swift.min(currentLeading, bounds.minX)
}
let top = self.reduce(0) { currentTop, bounds in
Swift.min(currentTop, bounds.minY)
}
let trailing = self.reduce(0) { currentTrailing, bounds in
Swift.max(currentTrailing, bounds.maxX)
}
let bottom = self.reduce(0) { currentBottom, bounds in
Swift.max(currentBottom, bounds.maxY)
}
return .init(x: leading, y: top, width: trailing - leading, height: bottom - top)
}
}

public struct MyZStack<Content>: View where Content: View {
let alignment: Alignment
let content: Content
public init(alignment: Alignment = .center, @ViewBuilder content: () -> Content) {
self.alignment = alignment
self.content = content()
}

public var body: some View {
_MyZStackLayout(alignment: alignment)() {
content
}
}
}