代码之家  ›  专栏  ›  技术社区  ›  NDR

在SwiftUI中,程序滚动到以像素表示的相对于前导锚点的偏移

  •  0
  • NDR  · 技术社区  · 2 年前

    我正在努力实现 this SwiftUI中的某种功能。基本想法是我有一个滑块和一个 ScrollView ,移动滑块可以滚动 卷轴视图 内容(不一定是上述示例中所示的视频,它可以是任何视图)。

    当然 ScrollViewReader 不包裹 UIScrollView.setContentOffset 并且仅提供 scrollTo(_ id: Hashable [, anchor: UnitPoint]) ,这并不能让我随心所欲地控制偏移量。

    在我看来 Slider 将生成一个介于0和1之间的数字,然后使用 lerp 滑块 的绑定,其中 lerp 可能如下:

    extension Range where Bound: BinaryFloatingPoint {
        func lerp(_ t: Bound) -> Bound {
            return (1-t)*self.lowerBound + t*self.upperBound
        }
    }
    
    extension ClosedRange where Bound: BinaryFloatingPoint {
        func lerp(_ t: Bound) -> Bound {
            return (1-t)*self.lowerBound + t*self.upperBound
        }
    }
    

    所以我试图绕过 ScrollViewReader 尽量避免 Introspect UIViewRepresentable / UIViewControllerRepresentable ,其流程如下:

    1. 向ScrollView的内容添加覆盖,覆盖整个大小 frame(maxWidth: .infinity, maxHeight: .infinity) 并且是 .leading 对齐。
    2. 覆盖将包含一个简单的 Rectangle() 具有 .clear 背景和 .contentShape(Rectangle()) ,其相对于 主要的 的锚 卷轴视图 的内容使用 @State private var offsetAnchor: CGFloat 变量,最初 .zero 以及分配给它的静态id,例如 .id("scrollAnchor")
    3. 当我想移动 卷轴视图 到给定的偏移量,我首先设置 offsetAnchor 到所需的偏移量,然后调用 scrollProxy.scrollTo(scrollAnchor)

    不幸的是,解决方法没有起作用,它似乎滚动到随机偏移(这并不意味着它实际上是随机的,只是我不知道标准)。以下是我的想法的一个非常基本的实现

    struct ContentView: View {
        
        @State private var inputValue: String = ""
        @State private var contentOffset: CGFloat = .zero {
            didSet {
                self.updateOffset("scrollAnchor")
            }
        }
        @State private var updateOffset: (String) -> Void = { _ in }
        
        var body: some View {
            VStack {
                ScrollViewReader { scrollProxy in
                    ScrollView(.horizontal, showsIndicators: true) {
                        Rectangle()
                            .fill(Colors.teal500)
                            .frame(width: 1000, height: 100)
                            .overlay(
                                ZStack {
                                    ForEach(1..<40) { i in
                                        VStack(alignment: .leading) {
                                            Rectangle()
                                                .fill(Colors.white)
                                                .frame(width: 1, height: 10)
                                            
                                            Text("\(i*25)")
                                                .font(.caption)
                                        }
                                        .offset(x: CGFloat(i*25))
                                    }
                                }
                                .fillMaxWidth(alignment: .leading)
                            ,alignment: .topLeading)
                            .overlay(
                                ZStack {
                                    Rectangle()
                                        .fill(Color.red)
                                        .contentShape(Rectangle())
                                        .frame(width: 1, height: 100)
                                        .position(x: self.contentOffset, y: 50)
                                        .onAppear {
                                            self.updateOffset = { id in
                                                DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                                                    scrollProxy.scrollTo(id)
                                                }
                                            }
                                                
                                        }
                                        .id("scrollAnchor")
    
                                }
                                    .fillMaxWidth(alignment: .leading)
                                , alignment: .leading
                            )
                    }
                }
                
                TextField(
                        "Scroll offset:",
                        text: self.$inputValue
                    )
                .keyboardType(.decimalPad)
                
                Button(action: {
                    self.contentOffset = CGFloat(Int(inputValue) ?? 0)
                }) {
                    Text("Scroll to input offset")
                }
    
            }
            .fillMaxSize(alignment: .leading)
        }
    }
    

    哪里 Colors.teal500 #319795 颜色,以及 fillMaxWidth() / fillMaxHeight() / fillMaxSize() 实现方式如下:

    struct FillMaxSize: ViewModifier {
        private var alignment: Alignment?
        
        init(alignment: Alignment?) {
            self.alignment = alignment
        }
        
        func body(content: Content) -> some View {
            if self.alignment != nil {
                return content
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: self.alignment!)
            } else {
                return content
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
            }
        }
    }
    
    struct FillMaxWidth: ViewModifier {
        private var alignment: Alignment?
        
        init(alignment: Alignment?) {
            self.alignment = alignment
        }
        
        func body(content: Content) -> some View {
            if self.alignment != nil {
                return content
                    .frame(minWidth: 0, maxWidth: .infinity, alignment: self.alignment!)
            } else {
                return content
                    .frame(minWidth: 0, maxWidth: .infinity)
            }
        }
    }
    
    struct FillMaxHeight: ViewModifier {
        private var alignment: Alignment?
        
        init(alignment: Alignment?) {
            self.alignment = alignment
        }
        
        func body(content: Content) -> some View {
            if self.alignment != nil {
                return content
                    .frame(minHeight: 0, maxHeight: .infinity, alignment: self.alignment!)
            } else {
                return content
                    .frame(minHeight: 0, maxHeight: .infinity)
            }
        }
    }
    
    extension View {
        func fillMaxSize(alignment: Alignment? = nil) -> some View {
            return self.modifier(FillMaxSize(alignment: alignment))
        }
        
        func fillMaxWidth(alignment: Alignment? = nil) -> some View {
            return self.modifier(FillMaxWidth(alignment: alignment))
        }
        
        func fillMaxHeight(alignment: Alignment? = nil) -> some View {
            return self.modifier(FillMaxHeight(alignment: alignment))
        }
    }
    

    我需要帮助来纠正我的实现中的错误(我怀疑 scrollTo 可能会在“scrollAnchor”移动到新位置之前调用,但情况似乎并非如此,因为我尝试执行 滚动到 延迟几秒钟的操作,并且没有任何更改),或者一些提示来实现这一点。

    非常感谢!!

    0 回复  |  直到 2 年前
        1
  •  0
  •   Benzy Neez    2 年前

    我普遍发现 ScrollViewProxy 需要通过保持层次结构的扁平化来尽可能多地帮助,避免对滚动到的id的确切位置产生任何可能的混淆 HStack VStack 布局设计,避免位置的偏移或其他调整。

    您已经解释过要水平滚动。因此,我建议将身体的主要内容用于校准 ScrollView 基于简单 HStack ,然后使用覆盖感兴趣的内容 .overlay .覆盖层自动采用主要内容的大小:

    struct ContentView: View {
    
        @State private var inputValue: String = ""
        @State private var contentOffset = 0.0
    
        var body: some View {
            GeometryReader { geometryProxy in
                VStack {
                    ScrollViewReader { scrollProxy in
                        ScrollView(.horizontal, showsIndicators: true) {
                            HStack(spacing: 0) {
    
                                // The main content consists of a row of rectangles
                                // that are 1-pixel wide, each associated with an
                                // identifier that corresponds to the index position
                                ForEach(0..<1000, id: \.self) { pixel in
                                    Rectangle()
                                        .frame(width: 1)
                                        .frame(maxHeight: .infinity)
                                }
                            }
                            .overlay(
    
                                // The scale is shown as an overlay
                                HStack(spacing: 0) {
                                    ForEach(0..<40) { i in
                                        VStack(alignment: .leading) {
                                            Rectangle()
                                                .fill(Color.white)
                                                .frame(width: 1, height: 10)
    
                                            Text("\(i*25)")
                                                .font(.caption)
                                                .allowsTightening(true)
                                                .minimumScaleFactor(0.1)
    
                                            Spacer()
                                        }
                                        .frame(width: 25, alignment: .leading)
                                    }
                                }
                                .background(Color.teal)
                            )
                        }
                        .frame(height: 100)
                        .onAppear {
                            scrollProxy.scrollTo(0, anchor: .leading)
                        }
                        .onChange(of: contentOffset) { newOffset in
                            scrollProxy.scrollTo(Int(newOffset), anchor: .leading)
                        }
                    }
                    // A slider for adjusting the scroll position interactively
                    Slider(
                        value: $contentOffset,
                        in: 0...(1000 - geometryProxy.size.width),
                        step: 1
                    )
                    .padding()
    
                    // Allow an explicit scroll position to be entered manually
                    TextField(
                        "Target offset",
                        text: $inputValue
                    )
                    .frame(width: 100)
                    .keyboardType(.decimalPad)
    
                    Button("Scroll to target offset") {
                        contentOffset = Double(inputValue) ?? 0.0
                    }
                }
            }
            .frame(height: 400)
        }
    }