这是一个允许使用夹点调整水平和垂直大小的示例。拖动紫色夹点可水平调整大小,垂直拖动橙色夹点。垂直和水平尺寸都受设备分辨率的限制。红色窗格始终可见,但可以使用切换隐藏夹点和其他窗格。还有一个reset按钮可以恢复,只有在原来的状态改变时才可见。还有其他有用的花絮和评论内联。
// Resizable panes, red is always visible
struct PanesView: View {
static let startWidth = UIScreen.main.bounds.size.width / 6
static let startHeight = UIScreen.main.bounds.size.height / 5
// update drag width when the purple grip is dragged
@State private var dragWidth : CGFloat = startWidth
// update drag height when the orange grip is dragged
@State private var dragHeight : CGFloat = startHeight
// remember show/hide green and blue panes
@AppStorage("show") var show : Bool = true
// keeps the panes a reasonable size based on device resolution
var minWidth : CGFloat = UIScreen.main.bounds.size.width / 6
let minHeight : CGFloat = UIScreen.main.bounds.size.height / 5
// purple and orange grips are this thick
let thickness : CGFloat = 9
// computed property that shows resize when appropriate
var showResize : Bool {
dragWidth != PanesView.startWidth || dragHeight != PanesView.startHeight
}
// use computed properties to keep the body tidy
var body: some View {
HStack(spacing: 0) {
redPane
// why two show-ifs? the animated one chases the non-animated and adds visual interest
if show {
purpleGrip
}
if show { withAnimation {
VStack(spacing: 0) {
greenPane
orangeGrip
Color.blue.frame(height: dragHeight) // blue pane
}
.frame(width: dragWidth)
} }
}
}
var redPane : some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
Color.red
// shows and hides the green and blue pane, both grips
Toggle(isOn: $show.animation(), label: {
// change icon depending on toggle position
Image(systemName: show ? "eye" : "eye.slash")
.font(.title)
.foregroundColor(.primary)
})
.frame(width: 100)
.padding()
}
}
var purpleGrip : some View {
Color.purple
.frame(width: thickness)
.gesture(
DragGesture()
.onChanged { gesture in
let screenWidth = UIScreen.main.bounds.size.width
// the framework feeds little deltas as the drag continues updating state
let delta = gesture.translation.width
// make sure drag width stays bounded
dragWidth = max(dragWidth - delta, minWidth)
dragWidth = min(screenWidth - thickness - minWidth, dragWidth)
}
)
}
var greenPane : some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
Color.green
// reset to original size
if showResize { withAnimation {
Button(action: { withAnimation {
dragWidth = UIScreen.main.bounds.size.width / 6
dragHeight = UIScreen.main.bounds.size.height / 5
} }, label: {
Image(systemName: "uiwindow.split.2x1")
.font(.title)
.foregroundColor(.primary)
.padding()
})
.buttonStyle(PlainButtonStyle())
}}
}
}
var orangeGrip : some View {
Color.orange
.frame(height: thickness)
.gesture(
DragGesture()
.onChanged { gesture in
let screenHeight = UIScreen.main.bounds.size.height
let delta = gesture.translation.height
dragHeight = max(dragHeight - delta, minHeight)
dragHeight = min(screenHeight - thickness - minHeight, dragHeight)
}
)
}
}
Run Code Online (Sandbox Code Playgroud)
我决定采用一种更像 SwiftUI 的方法。它可以是任何尺寸,因此不固定为整个屏幕尺寸。可以这样调用:
import SwiftUI
import ViewExtractor
struct ContentView: View {
var body: some View {
SeparatedStack(.vertical, ratios: [6, 4]) {
SeparatedStack(.horizontal, ratios: [2, 8]) {
Text("Top left")
Text("Top right")
}
SeparatedStack(.horizontal) {
Text("Bottom left")
Text("Bottom middle")
Text("Bottom right")
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
结果:
代码(阅读下面的注释):
// MARK: Extensions
extension Array {
subscript(safe index: Int) -> Element? {
guard indices ~= index else { return nil }
return self[index]
}
}
extension View {
@ViewBuilder func `if`<Output: View>(_ condition: Bool, transform: @escaping (Self) -> Output, else: @escaping (Self) -> Output) -> some View {
if condition {
transform(self)
} else {
`else`(self)
}
}
}
// MARK: Directional layout
enum Axes {
case horizontal
case vertical
}
private struct EitherStack<Content: View>: View {
let axes: Axes
let content: () -> Content
var body: some View {
switch axes {
case .horizontal: HStack(spacing: 0, content: content)
case .vertical: VStack(spacing: 0, content: content)
}
}
}
// MARK: Stacks
struct SeparatedStack: View {
static let dividerWidth: CGFloat = 5
static let minimumWidth: CGFloat = 20
private let axes: Axes
private let ratios: [CGFloat]?
private let views: [AnyView]
init<Views>(_ axes: Axes, ratios: [CGFloat]? = nil, @ViewBuilder content: TupleContent<Views>) {
self.axes = axes
self.ratios = ratios
views = ViewExtractor.getViews(from: content)
}
var body: some View {
GeometryReader { geo in
Color.clear
.overlay(SeparatedStackInternal(views: views, geo: geo, axes: axes, ratios: ratios))
}
}
}
// MARK: Stacks (internal)
private struct SeparatedStackInternal: View {
private struct GapBetween: Equatable {
let gap: CGFloat
let difference: CGFloat?
static func == (lhs: GapBetween, rhs: GapBetween) -> Bool {
lhs.gap == rhs.gap && lhs.difference == rhs.difference
}
}
@State private var dividerProportions: [CGFloat]
@State private var lastProportions: [CGFloat]
private let views: [AnyView]
private let geo: GeometryProxy
private let axes: Axes
init(views: [AnyView], geo: GeometryProxy, axes: Axes, ratios: [CGFloat]?) {
self.views = views
self.geo = geo
self.axes = axes
// Set initial proportions
if let ratios = ratios {
guard ratios.count == views.count else {
fatalError("Mismatching ratios array size. Should be same length as number of views.")
}
let total = ratios.reduce(0, +)
var proportions: [CGFloat] = []
for index in 0 ..< ratios.count - 1 {
let ratioTotal = ratios.prefix(through: index).reduce(0, +)
proportions.append(ratioTotal / total)
}
_dividerProportions = State(initialValue: proportions)
_lastProportions = State(initialValue: proportions)
} else {
let range = 1 ..< views.count
let new = range.map { index in
CGFloat(index) / CGFloat(views.count)
}
_dividerProportions = State(initialValue: new)
_lastProportions = State(initialValue: new)
}
}
var body: some View {
EitherStack(axes: axes) {
ForEach(views.indices) { index in
if index != 0 {
Color.gray
.if(axes == .horizontal) {
$0.frame(width: SeparatedStack.dividerWidth)
} else: {
$0.frame(height: SeparatedStack.dividerWidth)
}
}
let gapAtIndex = gapBetween(index: index)
views[index]
.if(axes == .horizontal) {
$0.frame(maxWidth: gapAtIndex.gap)
} else: {
$0.frame(maxHeight: gapAtIndex.gap)
}
.onChange(of: gapAtIndex) { _ in
if let difference = gapBetween(index: index).difference {
if dividerProportions.indices ~= index - 1 {
dividerProportions[index - 1] -= difference / Self.maxSize(axes: axes, geo: geo)
lastProportions[index - 1] = dividerProportions[index - 1]
}
}
}
}
}
.overlay(overlay(geo: geo))
}
@ViewBuilder private func overlay(geo: GeometryProxy) -> some View {
ZStack {
ForEach(dividerProportions.indices) { index in
Color(white: 0, opacity: 0.0001)
.if(axes == .horizontal) { $0
.frame(width: SeparatedStack.dividerWidth)
.position(x: lastProportions[index] * Self.maxSize(axes: axes, geo: geo))
} else: { $0
.frame(height: SeparatedStack.dividerWidth)
.position(y: lastProportions[index] * Self.maxSize(axes: axes, geo: geo))
}
.gesture(
DragGesture()
.onChanged { drag in
let translation = axes == .horizontal ? drag.translation.width : drag.translation.height
let currentPosition = lastProportions[index] * Self.maxSize(axes: axes, geo: geo) + translation
let offset = SeparatedStack.dividerWidth / 2 + SeparatedStack.minimumWidth
let minPos = highEdge(of: lastProportions, index: index - 1) + offset
let maxPos = lowEdge(of: lastProportions, index: index + 1) - offset
let newPosition = min(max(currentPosition, minPos), maxPos)
dividerProportions[index] = newPosition / Self.maxSize(axes: axes, geo: geo)
}
.onEnded { drag in
lastProportions[index] = dividerProportions[index]
}
)
}
}
.if(axes == .horizontal) {
$0.offset(y: geo.size.height / 2)
} else: {
$0.offset(x: geo.size.width / 2)
}
}
private static func maxSize(axes: Axes, geo: GeometryProxy) -> CGFloat {
switch axes {
case .horizontal: return geo.size.width
case .vertical: return geo.size.height
}
}
private func gapBetween(index: Int) -> GapBetween {
let low = lowEdge(of: dividerProportions, index: index)
let high = highEdge(of: dividerProportions, index: index - 1)
let gap = max(low - high, SeparatedStack.minimumWidth)
let difference = gap == SeparatedStack.minimumWidth ? SeparatedStack.minimumWidth - low + high : nil
return GapBetween(gap: gap, difference: difference)
}
private func lowEdge(of proportions: [CGFloat], index: Int) -> CGFloat {
var edge: CGFloat { proportions[index] * Self.maxSize(axes: axes, geo: geo) - SeparatedStack.dividerWidth / 2 }
return proportions[safe: index] != nil ? edge : Self.maxSize(axes: axes, geo: geo)
}
private func highEdge(of proportions: [CGFloat], index: Int) -> CGFloat {
var edge: CGFloat { proportions[index] * Self.maxSize(axes: axes, geo: geo) + SeparatedStack.dividerWidth / 2 }
return proportions[safe: index] != nil ? edge : 0
}
}
Run Code Online (Sandbox Code Playgroud)
注意:这使用我的GeorgeElsham/ViewExtractor来传递@ViewBuilder内容,而不仅仅是视图数组。这部分不是必需的,但我推荐它,因为它使代码可读并且更像 SwiftUI。