You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何构建类似ScrollViewReader/GeometryReader的代理并实现moveToTop功能?

实现CustomReader + CustomStack的视图置顶功能

问题背景

GeometryReader和ScrollViewReader的代理机制逻辑不透明,现有基于CustomReader和CustomStack Layout的垂直排列视图,需要实现调用moveToTop方法时,将指定ID的子视图移至顶部。

解决方案

核心思路是通过代理传递置顶目标ID,在Layout的布局逻辑中重新排序子视图,将目标视图放在最前列。以下是完整修改后的代码:

import SwiftUI

struct ContentView: View {
    var body: some View {
        CustomReader { proxy in
            CustomStack {
                ForEach(0..<10) { index in
                    Text("Sample \(index)")
                        .customId(index)
                }
            }
            .background(.red)
            .onAppear {
                proxy.moveToTop(id: 5)
            }
        }
        .padding()
    }
}

// 重构CustomProxy为ObservableObject,方便跨视图同步状态
class CustomProxy: ObservableObject, Equatable {
    static func == (lhs: CustomProxy, rhs: CustomProxy) -> Bool {
        lhs.size == rhs.size && lhs.targetId == rhs.targetId
    }

    @Published var size: CGSize
    @Published var targetId: (any Hashable)? // 存储需要置顶的视图ID

    init(size: CGSize = .zero) {
        self.size = size
        self.targetId = nil
    }

    // 对外暴露的置顶方法
    func moveToTop<ID: Hashable>(id: ID) {
        targetId = id
    }
}

// 修改CustomReader使用@StateObject管理代理,通过EnvironmentObject传递
struct CustomReader<Content: View>: View {
    var content: (CustomProxy) -> Content

    @StateObject private var proxy = CustomProxy()

    init(content: @escaping (CustomProxy) -> Content) {
        self.content = content
    }

    var body: some View {
        content(proxy)
            .environmentObject(proxy)
    }
}

// CustomStack通过EnvironmentObject获取代理,传递给Layout
struct CustomStack<Content: View>: View {
    var content: () -> Content

    @EnvironmentObject private var proxy: CustomProxy

    init(content: @escaping () -> Content) {
        self.content = content
    }

    var body: some View {
        CustomStackLayout(proxy: proxy) {
            content()
        }
        .frame(
            maxWidth: proxy.size.width == 0 ? .infinity : proxy.size.width,
            maxHeight: proxy.size.height == 0 ? .infinity : proxy.size.height
        )
    }
}

// 核心:在Layout中根据targetId重新排序子视图
struct CustomStackLayout: Layout {
    @ObservedObject var proxy: CustomProxy

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        proposal.replacingUnspecifiedDimensions()
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        var containerRect: CGRect = CGRect(origin: bounds.origin, size: .zero)
        var yPos: CGFloat = 0

        // 根据targetId排序子视图
        let targetId = proxy.targetId
        var sortedSubviews: Subviews
        if let targetId = targetId {
            // 找出目标视图
            let targetSubview = subviews.first { subview in
                subview[CustomIDLayoutValueKey.self] == targetId
            }
            // 构造排序后的数组:目标视图在前,其余保持原顺序
            sortedSubviews = []
            if let target = targetSubview {
                sortedSubviews.append(target)
                sortedSubviews.append(contentsOf: subviews.filter { $0.id != target.id })
            } else {
                sortedSubviews = subviews
            }
            // 重置targetId,避免重复触发置顶
            proxy.targetId = nil
        } else {
            sortedSubviews = subviews
        }

        // 按新顺序放置子视图
        for subview in sortedSubviews {
            let subviewSize = subview.sizeThatFits(proposal)
            let subviewPoint = CGPoint(x: bounds.minX, y: bounds.minY + yPos)

            subview.place(at: subviewPoint, proposal: proposal)
            yPos += subviewSize.height

            containerRect.size.width = max(containerRect.size.width, subviewSize.width)
            containerRect.size.height += subviewSize.height
        }

        proxy.size = containerRect.size
    }
}

// 保持原有ID标记逻辑
struct CustomIDLayoutValueKey: LayoutValueKey {
    static let defaultValue: (any Hashable)? = nil
}

extension View {
    func customId(_ value: (any Hashable)?) -> some View {
        layoutValue(key: CustomIDLayoutValueKey.self, value: value)
    }
}

#Preview {
    ContentView()
}

关键改动说明

  1. CustomProxy重构:从struct改为ObservableObject,通过@Published属性自动同步状态,避免值类型的副本修改问题。
  2. 代理传递优化:使用EnvironmentObject替代自定义环境键,简化跨视图的状态传递逻辑。
  3. Layout排序逻辑:在placeSubviews中根据targetId筛选目标视图,将其放在数组最前列,其余视图保持原顺序,完成置顶效果。
  4. 状态重置:置顶完成后清空targetId,防止重复触发布局排序。

内容的提问来源于stack exchange,提问作者Mark

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 16:13:09