SwiftUI Создание настраиваемого сегментированного элемента управления также в ScrollView

Ниже мой код для создания стандартного сегментированного элемента управления.

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: когда вы выбираете ресторан, вы можете перейти к определенной части меню, выбрав опцию в настраиваемом сегментированном элементе управления.

Включены элементы, которые я хочу настроить:

введите описание изображения здесь

* ОБНОВЛЕНИЕ *

Изображение окончательного дизайна

введите описание изображения здесь


person Learn2Code    schedule 22.03.2020    source источник
comment
Для настраиваемого управления сегментами вы можете реализовать его по протоколу UIViewRepresentable.   -  person Mac3n    schedule 22.03.2020
comment
@ Mac3n это не очень помогает   -  person Learn2Code    schedule 23.03.2020
comment
Можете ли вы добавить изображение, показывающее, как вы хотите, чтобы сборщик выглядел?   -  person LuLuGaGa    schedule 25.03.2020
comment
@LuLuGaGa Я обновил вопрос изображением конечного продукта   -  person Learn2Code    schedule 25.03.2020
comment
@LuLuGaGa, могу я задать один вопрос, я помещаю его в горизонтальную прокрутку, знаете ли вы, как я могу отрегулировать смещение, чтобы выбранный сегмент не был обрезан, если пользователь выберет выделение наполовину обрезанным?   -  person Learn2Code    schedule 15.04.2020


Ответы (3)


Это то, что вы ищите?

введите описание изображения здесь

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()
  }
}
person nine stones    schedule 25.03.2020
comment
это именно то, о чем я имел в виду. Вопрос: а что, если мне это нужно в представлении с горизонтальной прокруткой, так как у меня было больше вариантов, чем те три, которые я упомянул, и я хотел бы охватить размер экрана. - person Learn2Code; 25.03.2020
comment
Представление --kinda - по-прежнему работает при переносе в горизонтальный ScrollView. Но он, конечно, не регулирует положение прокрутки, что делает его не совсем полезным. Возможно, вы сможете почерпнуть некоторые идеи о том, как это сделать, из этого сообщения: stackoverflow.com/questions/57258846/ - person nine stones; 25.03.2020
comment
Чтобы прокрутить горизонтальный ScrollView: на самом деле я только что наткнулся на другой пост SO от того же пользователя: stackoverflow.com/questions/60855852/ - person nine stones; 25.03.2020
comment
Замечательный ответ. Я бы просто заменил анимацию по умолчанию на .easeInOut(duration: 0.2), чтобы она выглядела еще более похожей на анимацию Picker. - person andrés; 10.10.2020
comment
Для тех, кто столкнется с этим в будущем, поскольку измерения для представлений выполняются только на onAppear, он перестанет работать, если макет изменится. Решение здесь: stackoverflow.com/a/66512870/560942 - person jnpdx; 07.03.2021

Если я правильно понимаю вопрос, отправной точкой может быть что-то вроде приведенного ниже кода. Стиль, очевидно, требует немного внимания. У этого есть фиксированная ширина для сегментов. Чтобы быть более гибким, вам нужно использовать Geometry Reader, чтобы измерить то, что было доступно, и разделить пространство.

введите здесь описание изображения

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()
    }
}
person Obliquely    schedule 25.03.2020
comment
спасибо за ваше время, но вышеприведенный ответ был мертв - person Learn2Code; 25.03.2020
comment
Другой ответ действительно хорош, особенно то, как он воспроизводит анимацию выбора. - person Obliquely; 25.03.2020

Ни одно из вышеперечисленных решений не сработало для меня, поскольку 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)
            }
        }
    }
}
person UltAngel    schedule 13.11.2020