0

I am developing an app with a custom dock and I am unsure of how to change the view when highlighted on an item in the dock. I just couldn't find a way to switch the view when you highlight the item. I have attempted methods such as a switch statement but that did not work in my scenario. I have also attempted to use an if-else statement but that also did not work. I would much appreciate your help in finding a solution to this issue. Please review my code below...

struct MathematicallyController: View {
@State var selection: Int = 1
var body: some View {
    ZStack {
        ZStack {
            if selection == 0 {
                //view 1
            } else if selection == 1 {
                //view 2
            } else if selection == 2 {
                //view 3
            } else {
                //view 1
            }
        }
        .overlay(
            VStack {
                Spacer()
                ZStack {
                    BlurView(style: .systemThinMaterialDark)
                        .frame(maxWidth: .infinity, maxHeight: 65)
                        .cornerRadius(20)
                        .padding()
                    RoundedRectangle(cornerRadius: 20)
                        .stroke(lineWidth: 0.9)
                        .frame(maxWidth: .infinity, maxHeight: 65)
                        .blur(radius: 2)
                        .padding()
                    Picker(selection: selection)
                        .padding(5)
                }
                .offset(y: 30)
            }
        )
    }
}

} Picker

extension View {
func eraseToAnyView() -> AnyView {
    AnyView(self)
}

}

struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
    value = nextValue()
}

}

struct BackgroundGeometryReader: View {
var body: some View {
    GeometryReader { geometry in
        return Color
                .clear
                .preference(key: SizePreferenceKey.self, value: geometry.size)
    }
}

}

struct SizeAwareViewModifier: ViewModifier {

@Binding private var viewSize: CGSize

init(viewSize: Binding<CGSize>) {
    self._viewSize = viewSize
}

func body(content: Content) -> some View {
    content
        .background(BackgroundGeometryReader())
        .onPreferenceChange(SizePreferenceKey.self, perform: { if self.viewSize != $0 { self.viewSize = $0 }})
}

}

struct SegmentedPicker: View {
private static let ActiveSegmentColor: Color = Color(.tertiarySystemBackground)
private static let BackgroundColor: Color = Color(.secondarySystemBackground)
private static let ShadowColor: Color = Color.white.opacity(0.2)
private static let TextColor: Color = Color(.secondaryLabel)
private static let SelectedTextColor: Color = Color(.label)

private static let TextFont: Font = .system(size: 12)

private static let SegmentCornerRadius: CGFloat = 12
private static let ShadowRadius: CGFloat = 10
private static let SegmentXPadding: CGFloat = 16
private static let SegmentYPadding: CGFloat = 9
private static let PickerPadding: CGFloat = 7

private static let AnimationDuration: Double = 0.2

// Stores the size of a segment, used to create the active segment rect
@State private var segmentSize: CGSize = .zero
// Rounded rectangle to denote active segment
private var activeSegmentView: AnyView {
    // Don't show the active segment until we have initialized the view
    // This is required for `.animation()` to display properly, otherwise the animation will fire on init
    let isInitialized: Bool = segmentSize != .zero
    if !isInitialized { return EmptyView().eraseToAnyView() }
    return
        RoundedRectangle(cornerRadius: SegmentedPicker.SegmentCornerRadius)
            .fill(.regularMaterial)
            .shadow(color: SegmentedPicker.ShadowColor, radius: SegmentedPicker.ShadowRadius)
            .frame(width: self.segmentSize.width, height: self.segmentSize.height)
            .offset(x: self.computeActiveSegmentHorizontalOffset(), y: 0)
            .animation(Animation.linear(duration: SegmentedPicker.AnimationDuration))
            .overlay(
                RoundedRectangle(cornerRadius: SegmentedPicker.SegmentCornerRadius)
                    .stroke(lineWidth: 1)
                    .shadow(color: SegmentedPicker.ShadowColor, radius: SegmentedPicker.ShadowRadius)
                    .frame(width: self.segmentSize.width, height: self.segmentSize.height)
                    .offset(x: self.computeActiveSegmentHorizontalOffset(), y: 0)
                    .animation(Animation.linear(duration: SegmentedPicker.AnimationDuration))
            )
            .eraseToAnyView()
}

@Binding private var selection: Int
private let items: [Image]

init(items: [Image], selection: Binding<Int>) {
    self._selection = selection
    self.items = items
}

var body: some View {
    // Align the ZStack to the leading edge to make calculating offset on activeSegmentView easier
    ZStack(alignment: .leading) {
        // activeSegmentView indicates the current selection
        self.activeSegmentView
        HStack {
            ForEach(0..<self.items.count, id: \.self) { index in
                self.getSegmentView(for: index)
            }
        }
    }
    .padding(SegmentedPicker.PickerPadding)
    .background(.regularMaterial)
    .clipShape(RoundedRectangle(cornerRadius: SegmentedPicker.SegmentCornerRadius))
}

// Helper method to compute the offset based on the selected index
private func computeActiveSegmentHorizontalOffset() -> CGFloat {
    CGFloat(self.selection) * (self.segmentSize.width + SegmentedPicker.SegmentXPadding / 2)
}

// Gets text view for the segment
private func getSegmentView(for index: Int) -> some View {
    guard index < self.items.count else {
        return EmptyView().eraseToAnyView()
    }
    let isSelected = self.selection == index
    return
        Text(self.items[index])
            // Dark test for selected segment
            .foregroundColor(isSelected ? SegmentedPicker.SelectedTextColor: SegmentedPicker.TextColor)
            .lineLimit(1)
            .padding(.vertical, SegmentedPicker.SegmentYPadding)
            .padding(.horizontal, SegmentedPicker.SegmentXPadding)
            .frame(minWidth: 0, maxWidth: .infinity)
            // Watch for the size of the
            .modifier(SizeAwareViewModifier(viewSize: self.$segmentSize))
            .onTapGesture { self.onItemTap(index: index) }
            .eraseToAnyView()
}

// On tap to change the selection
private func onItemTap(index: Int) {
    guard index < self.items.count else {
        return
    }
    self.selection = index
}

}

struct Picker: View {
@State var selection: Int = 1
private let items: [Image] = [Image(systemName: "rectangle.on.rectangle"), Image(systemName: "timelapse"), Image(systemName: "plus")]

var body: some View {
    SegmentedPicker(items: self.items, selection: self.$selection)
        .padding()
}

}

0

1 Answer 1

0

In your Picker struct you are getting selection as a value not a Binding.

The purpose of using a Binding variable is to make the parent of the passed variable listen to the changes made in the struct. In other words, it binds the 2 views/values.

So what you should do is modify Picker like this:

struct Picker: View {
    @Binding var selection: Int = 1
    private let items: [Image] = [Image(systemName: "rectangle.on.rectangle"), Image(systemName: "timelapse"), Image(systemName: "plus")]
    var body: some View {
        SegmentedPicker(items: self.items, selection: self.$selection)
        .padding()
    }
}

And in MathematicallyController change Picker(selection: selection) into Picker(selection: $selection) so you'd have:

struct MathematicallyController: View {
    @State var selection: Int = 1
    var body: some View {
        ZStack {
            ZStack {
                if selection == 0 {
                    //view 1
                } else if selection == 1 {
                    //view 2
                } else if selection == 2 {
                    //view 3
                } else {
                    //view 1
                }
            }
            .overlay(
                VStack {
                    Spacer()
                    ZStack {
                        BlurView(style: .systemThinMaterialDark)
                            .frame(maxWidth: .infinity, maxHeight: 65)
                            .cornerRadius(20)
                            .padding()
                        RoundedRectangle(cornerRadius: 20)
                            .stroke(lineWidth: 0.9)
                            .frame(maxWidth: .infinity, maxHeight: 65)
                            .blur(radius: 2)
                            .padding()
                        Picker(selection: $selection)
                            .padding(5)
                    }
                    .offset(y: 30)
                }
            )
        }
    }
}

Also Note that a switch statement will work just as fine as an if one.

9
  • I am getting an error that says, "Cannot convert value of type 'Int' to specified type 'Binding<Int>'" on the new Picker. It is pointing to the "@State var selection: Int = 1".
    – user17136166
    Commented Jun 20, 2022 at 10:11
  • It also says "Incorrect argument label in call (have 'wrappedValue:', expected 'projectedValue:')". And tells me to "Replace '1' with 'projectedValue'" and when i do that it says "Cannot find 'projectedValue' in scope".
    – user17136166
    Commented Jun 20, 2022 at 10:13
  • did you add the $?
    – Timmy
    Commented Jun 20, 2022 at 10:15
  • When I did that, it said: "Cannot convert value '$selection' of type 'Binding<Int>' to expected type 'Int', use wrapped value instead" and recommended me to remove the $
    – user17136166
    Commented Jun 20, 2022 at 10:16
  • Did you change the picker like in my answer?
    – Timmy
    Commented Jun 20, 2022 at 10:17