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()
}
}
CodePudding user response:
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.