ios SwiftUI也在ScrollView中创建自定义分段控件

50few1ms  于 2023-06-25  发布在  iOS
关注(0)|答案(4)|浏览(85)

下面是我创建一个标准分段控件的代码。

struct ContentView: View {

    @State private var favoriteColor = 0
    var colors = ["Red", "Green", "Blue"]

    var body: some View {
        VStack {
            Picker(selection: $favoriteColor, label: Text("What is your favorite color?")) {
                ForEach(0..<colors.count) { index in
                    Text(self.colors[index]).tag(index)
                }
            }.pickerStyle(SegmentedPickerStyle())

            Text("Value: \(colors[favoriteColor])")
        }
    }
}

我的问题是,我如何修改它来拥有一个自定义的分段控件,在那里我可以让边界与我自己的颜色沿着变圆,因为这在UIKit中很容易做到?有人这样做过吗。
我最好的例子是Uber eats应用程序,当你选择一家餐厅时,你可以通过在定制的分段控件中选择一个选项滚动到菜单的特定部分。
包括我期待定制的元素:

更新

最终设计图片

6jygbczu

6jygbczu1#

这是你要找的吗?

import SwiftUI

struct CustomSegmentedPickerView: View {
  @State private var selectedIndex = 0
  private var titles = ["Round Trip", "One Way", "Multi-City"]
  private var colors = [Color.red, Color.green, Color.blue]
  @State private var frames = Array<CGRect>(repeating: .zero, count: 3)

  var body: some View {
    VStack {
      ZStack {
        HStack(spacing: 10) {
          ForEach(self.titles.indices, id: \.self) { index in
            Button(action: { self.selectedIndex = index }) {
              Text(self.titles[index])
            }.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)).background(
              GeometryReader { geo in
                Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
              }
            )
          }
        }
        .background(
          Capsule().fill(
            self.colors[self.selectedIndex].opacity(0.4))
            .frame(width: self.frames[self.selectedIndex].width,
                   height: self.frames[self.selectedIndex].height, alignment: .topLeading)
            .offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
          , alignment: .leading
        )
      }
      .animation(.default)
      .background(Capsule().stroke(Color.gray, lineWidth: 3))

      Picker(selection: self.$selectedIndex, label: Text("What is your favorite color?")) {
        ForEach(0..<self.titles.count) { index in
          Text(self.titles[index]).tag(index)
        }
      }.pickerStyle(SegmentedPickerStyle())

      Text("Value: \(self.titles[self.selectedIndex])")
      Spacer()
    }
  }

  func setFrame(index: Int, frame: CGRect) {
    self.frames[index] = frame
  }
}

struct CustomSegmentedPickerView_Previews: PreviewProvider {
  static var previews: some View {
    CustomSegmentedPickerView()
  }
}
n3ipq98p

n3ipq98p2#

如果我没理解错的话,起点可能类似于下面的代码。造型,显然需要一点注意。这具有段的硬连线宽度。为了更灵活,你需要使用几何阅读器来测量可用的空间并划分空间。

struct ContentView: View {

      @State var selection = 0

      var body: some View {

            let item1 = SegmentItem(title: "Some Way", color: Color.blue, selectionIndex: 0)
            let item2 = SegmentItem(title: "Round Zip", color: Color.red, selectionIndex: 1)
            let item3 = SegmentItem(title: "Multi-City", color: Color.green, selectionIndex: 2)

            return VStack() {
                  Spacer()
                  Text("Selected Item: \(selection)")
                  SegmentControl(selection: $selection, items: [item1, item2, item3])
                  Spacer()
            }
      }
}

struct SegmentControl : View {

      @Binding var selection : Int
      var items : [SegmentItem]

      var body : some View {

            let width : CGFloat = 110.0

            return HStack(spacing: 5) {
                  ForEach (items, id: \.self) { item in
                        SegmentButton(text: item.title, width: width, color: item.color, selectionIndex: item.selectionIndex, selection: self.$selection)
                  }
            }.font(.body)
                  .padding(5)
                  .background(Color.gray)
                  .cornerRadius(10.0)
      }
}

struct SegmentButton : View {

      var text : String
      var width : CGFloat
      var color : Color
      var selectionIndex = 0
      @Binding var selection : Int

      var body : some View {
            let label = Text(text)
                  .padding(5)
                  .frame(width: width)
                  .background(color).opacity(selection == selectionIndex ? 1.0 : 0.5)
                  .cornerRadius(10.0)
                  .foregroundColor(Color.white)
                  .font(Font.body.weight(selection == selectionIndex ? .bold : .regular))

            return Button(action: { self.selection = self.selectionIndex }) { label }
      }
}

struct SegmentItem : Hashable {
      var title : String = ""
      var color : Color = Color.white
      var selectionIndex = 0
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
zpgglvta

zpgglvta3#

上述解决方案对我来说都不起作用,因为GeometryReader一旦放置在导航视图中就会返回不同的值,这会抛出活动指示器在背景中的定位。我找到了其他的解决方案,但它们只适用于固定长度的菜单字符串。也许有一个简单的修改可以使上面的代码工作,如果是的话,我会迫不及待地阅读它。如果你和我有同样的问题,那么这可能会为你工作。
多亏了Reddit用户“End3r117”的灵感和这篇SwiftWithMajid文章https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/,我能够制定一个解决方案。它可以在NavigationView内部或外部工作,并接受各种长度的菜单项。

struct SegmentMenuPicker: View {
    var titles: [String]
    var color: Color
    
    @State private var selectedIndex = 0
    @State private var frames = Array<CGRect>(repeating: .zero, count: 5)

    var body: some View {
        VStack {
            ZStack {
                HStack(spacing: 10) {
                    ForEach(self.titles.indices, id: \.self) { index in
                        Button(action: {
                            print("button\(index) pressed")
                            self.selectedIndex = index
                        }) {
                            Text(self.titles[index])
                                .foregroundColor(color)
                                .font(.footnote)
                                .fontWeight(.semibold)
                        }
                        .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
                        .modifier(FrameModifier())
                        .onPreferenceChange(FramePreferenceKey.self) { self.frames[index] = $0 }
                    }
                }
                .background(
                    Rectangle()
                        .fill(self.color.opacity(0.4))
                        .frame(
                            width: self.frames[self.selectedIndex].width,
                            height: 2,
                            alignment: .topLeading)
                        .offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX, y: self.frames[self.selectedIndex].height)
                    , alignment: .leading
                )
            }
            .padding(.bottom, 15)
            .animation(.easeIn(duration: 0.2))

            Text("Value: \(self.titles[self.selectedIndex])")
            Spacer()
        }
    }
}

struct FramePreferenceKey: PreferenceKey {
    static var defaultValue: CGRect = .zero

    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

struct FrameModifier: ViewModifier {
    private var sizeView: some View {
        GeometryReader { geometry in
            Color.clear.preference(key: FramePreferenceKey.self, value: geometry.frame(in: .global))
        }
    }

    func body(content: Content) -> some View {
        content.background(sizeView)
    }
}

struct NewPicker_Previews: PreviewProvider {
    static var previews: some View {
        VStack {
            SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.blue)
            NavigationView {
                SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.red)
            }
        }
    }
}
ttvkxqim

ttvkxqim4#

由于上面没有任何解决方案可以解决本地解决方案的感觉,我基于上面的实现创建了自己的解决方案。https://github.com/poromaa/swiftui-capsule-picker/tree/main

import SwiftUI

struct CapsulePicker: View {
    @Binding var selectedIndex: Int
    @State private var hoverIndex = 0
    @State private var dragOffset: CGFloat = 0
    @State private var optionWidth: CGFloat = 0
    @State private var totalSize: CGSize = .zero
    @State private var isDragging: Bool = false
    let titles: [String]
    
    var body: some View {
        ZStack(alignment: .leading) {
            Capsule()
                .fill(Color.accentColor)
                .padding(isDragging ? 2 : 0)
                .frame(width: optionWidth, height: totalSize.height)
                .offset(x: dragOffset)
                .gesture(
                    LongPressGesture(minimumDuration: 0.01)
                        .sequenced(before: DragGesture())
                        .onChanged { value in
                            switch value {
                            case .first(true):
                                isDragging = true
                            case .second(true, let drag):
                                let translationWidth = (drag?.translation.width ?? 0) + CGFloat(selectedIndex) * optionWidth
                                hoverIndex = Int(round(min(max(0, translationWidth), optionWidth * CGFloat(titles.count - 1)) / optionWidth))
                            default:
                                isDragging = false
                            }
                        }
                        .onEnded { value in
                            if case .second(true, let drag?) = value {
                                let predictedEndOffset = drag.translation.width + CGFloat(selectedIndex) * optionWidth
                                selectedIndex = Int(round(min(max(0, predictedEndOffset), optionWidth * CGFloat(titles.count - 1)) / optionWidth))
                                hoverIndex = selectedIndex
                            }
                            isDragging = false
                        }
                        .simultaneously(with: TapGesture().onEnded { _ in isDragging = false })
                )
            
                .animation(.spring(), value: dragOffset)
                .animation(.spring(), value: isDragging)
            
            Capsule().fill(Color.accentColor).opacity(0.2)
                .padding(-4)
            
            HStack(spacing: 0) {
                ForEach(titles.indices, id: \.self) { index in
                    Text(titles[index])
                        .frame(width: optionWidth, height: totalSize.height, alignment: .center)
                        .foregroundColor(hoverIndex == index ? .white : .black)
                        .animation(.easeInOut, value: hoverIndex)
                        .font(.system(size: 14, weight: .bold))
                    
                        .contentShape(Capsule())
                        .onTapGesture {
                            selectedIndex = index
                            hoverIndex = index
                        }.allowsHitTesting(selectedIndex != index)
                }
            }
            .onChange(of: hoverIndex) {i in
                dragOffset =  CGFloat(i) * optionWidth
            }
            .onChange(of: selectedIndex) {i in
                hoverIndex = i
            }
            .frame(width: totalSize.width, alignment: .leading)
        }
        .background(GeometryReader { proxy in Color.clear.onAppear { totalSize = proxy.size } })
        .onChange(of: totalSize) { _ in optionWidth = totalSize.width/CGFloat(titles.count) }
        .onAppear { hoverIndex = selectedIndex }
        .frame(height: 50)
        .padding([.leading, .trailing], 10)
    }
}

struct CapsulePickerPreview: View {
    @State private var selectedIndex = 0
    var titles = ["Red", "Greenas", "Blue"]
    var body: some View {
        VStack {
            CapsulePicker(selectedIndex: $selectedIndex, titles: titles)      .padding()
            Text("Selected index: \(selectedIndex)")
            
            VStack {
                Picker(selection: self.$selectedIndex, label: Text("What is your favorite color?")) {
                    ForEach(titles.indices, id: \.self) { index in
                        Text(self.titles[index]).tag(index)
                    }
                }.pickerStyle(SegmentedPickerStyle())
                
                
                
                Text("Value: \(self.titles[self.selectedIndex])")
                Spacer()
            }
        }
        .padding()
    }
}

struct CapsulePicker_Previews: PreviewProvider {
    static var previews: some View {
        CapsulePickerPreview()
    }
}

相关问题