I have a grid of DisclosureGroup enclosed in a 2 column LazyVGrid. The problem I am having is that when I expand the DisclosureGroup it overlaps the Text below the group. Is there a way to fix the overlap? Ideally, I would like the DisclosureGroupView (shown below) to push everything down.
I tried to use zIndex on the DisclosureGroup but it does not look good. It basically expands the entire grid which encloses the DisclosureGroup view.
Appreciate very much for any direction to fix this issue!
Here is what I have so far:
DisclosureGroupView:
struct DisclosureGroupView: View {
var groups: [String: [String]]
#State private var isExpanded: [String: Bool]
#State private var selectedItems = [String: Set<String>]()
#State private var isTitleChecked: [String: Bool]
init(groups: [String: [String]]) {
self.groups = groups
_isExpanded = State(initialValue: groups.reduce(into: [String: Bool](), { result, group in
result[group.key] = false
}))
_isTitleChecked = State(initialValue: groups.reduce(into: [String: Bool](), {result, group in
result[group.key] = false
}))
}
var body: some View {
GeometryReader { geo in
let ncols = 2
let width = geo.size.width / CGFloat(ncols)
let layout = Array(repeating: GridItem(.fixed(width), spacing: 5), count: ncols)
LazyVGrid(columns: layout, alignment: .leading) {
ForEach(groups.keys.sorted(), id: \.self) { title in
self.disclosureGroup(title: title, items: groups[title, default: [String]()])
}
}
}
}
private func disclosureGroup(title: String, items: [String]) -> some View {
let expanded = Binding<Bool>(get: {isExpanded[title]!}, set: {isExpanded[title] = $0})
return DisclosureGroup(title, isExpanded: expanded) {
ScrollView {
VStack(alignment: .leading) {
ForEach(Array(items.enumerated()), id: \.1.hashValue) { index, item in
Text(item)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.zIndex(1.0)
}
.frame(height: 60)
}
.font(.body)
.frame(width: 110)
.accentColor(.white)
.foregroundColor(.white)
.padding(5)
.background(Color(UIColor.systemBlue))
.cornerRadius(8.0)
}
}
ContentView to use the above view:
struct ContentView: View {
var body: some View {
VStack(alignment: .center, spacing: 10) {
Text("Disclosure Groups")
let groups: [String: [String]] = ["R1C1" : ["item 1", "item 2", "item 3", "item 4"],
"R1C2" : ["item 3", "item 4", "item 6", "item 7"],
"R2C1" : ["item 5", "item 6"],
"R2C2" : ["item 7", "item 8"]]
DisclosureGroupView(groups: groups)
.frame(height: 80)
Text("More Text here that is long")
.padding()
Text("Some More Text even wider than the first one")
}
}
}
Here is what I am getting:
StackOverFlow
The height is fixed because of the GeometryReader. I modified it by passing the width.
//
// SwiftUIView.swift
// asdadd
//
// Created by USER on 2021/08/04.
//
import SwiftUI
struct DisclosureGroupView: View {
var groups: [String: [String]]
var width: CGFloat
#State private var isExpanded: [String: Bool]
#State private var selectedItems = [String: Set<String>]()
#State private var isTitleChecked: [String: Bool]
init(groups: [String: [String]], width: CGFloat) {
self.groups = groups
self.width = width
_isExpanded = State(initialValue: groups.reduce(into: [String: Bool](), { result, group in
result[group.key] = false
}))
_isTitleChecked = State(initialValue: groups.reduce(into: [String: Bool](), {result, group in
result[group.key] = false
}))
}
var body: some View {
// GeometryReader { geo in
let ncols = 2
let width = width / CGFloat(ncols)
let layout = Array(repeating: GridItem(.fixed(width), spacing: 5), count: ncols)
LazyVGrid(columns: layout, alignment: .leading) {
ForEach(groups.keys.sorted(), id: \.self) { title in
self.disclosureGroup(title: title, items: groups[title, default: [String]()])
}
}
.border(Color.red)
// }
}
private func disclosureGroup(title: String, items: [String]) -> some View {
let expanded = Binding<Bool>(get: {isExpanded[title]!}, set: {isExpanded[title] = $0})
return DisclosureGroup(title, isExpanded: expanded) {
ScrollView {
VStack(alignment: .leading) {
ForEach(Array(items.enumerated()), id: \.1.hashValue) { index, item in
Text(item)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.zIndex(1.0)
}
}
.font(.body)
.frame(width: 110)
.accentColor(.white)
.foregroundColor(.white)
.padding(5)
.background(Color(UIColor.systemBlue))
.cornerRadius(8.0)
}
}
struct ContentView2: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center, spacing: 10) {
Text("Disclosure Groups")
let groups: [String: [String]] = ["R1C1" : ["item 1", "item 2", "item 3", "item 4"],
"R1C2" : ["item 3", "item 4", "item 6", "item 7"],
"R2C1" : ["item 5", "item 6"],
"R2C2" : ["item 7", "item 8"]]
DisclosureGroupView(groups: groups, width: geometry.size.width)
// .frame(height: 80)
Text("More Text here that is long")
.padding()
Text("Some More Text even wider than the first one")
}
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}
I don't know if VStack's alignment doesn't work.
I moved it to the center using position.
GeometryReader { geometry in
VStack(spacing: 10) {
Text("Disclosure Groups")
let groups: [String: [String]] = ["R1C1" : ["item 1", "item 2", "item 3", "item 4"],
"R1C2" : ["item 3", "item 4", "item 6", "item 7"],
"R2C1" : ["item 5", "item 6"],
"R2C2" : ["item 7", "item 8"]]
DisclosureGroupView(groups: groups, width: geometry.size.width)
// .frame(height: 80)
Text("More Text here that is long")
.padding()
Text("Some More Text even wider than the first one")
}
.position(x: geometry.frame(in:.local).midX,
y: geometry.frame(in:.local).midY)
}
Related
I have followed a tutorial in SWIFT UI (only just started using it) and I am trying to open new views using the same logic applied so far. Basically there is a tab bar with 5 views (Search ,home etc...) which works opening a new view with each tabbar item, however in my homeview page I have some button cards that I want to open a new view. I can get the text for selectedSection to work but it shows the Text over the top of the homeview. How can I get it to open a new view entirely?
Here is my content view:
struct ContentView: View {
#AppStorage("selectedTab") var selectedTab: Tab = .home
#AppStorage("selectedSection") var selectedSection: Features = .calculators
#State var isOpen = false
#State var show = false
let button = RiveViewModel(fileName: "menu_button", stateMachineName: "State
Machine", autoPlay: false)
var body: some View {
ZStack {
Color("Background 2").ignoresSafeArea()
SideMenu()
.opacity(isOpen ? 1 : 0)
.offset(x: isOpen ? 0 : -300)
.rotation3DEffect(.degrees(isOpen ? 0 : 30), axis: (x: 0, y: 1, z: 0))
Group{
switch selectedTab {
case .home:
HomeView()
case .search:
Text("Search")
case .star:
Text("Favorites")
case .bell:
Text("Bell")
case .user:
Text("User")
}
switch selectedSection {
case .calculators:
Text("Calculators")
case .projects:
Text("Projects")
case .kvFinder:
Text("kv Finder")
}
}
And my home view:
var content: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Welcome")
.customFont(.largeTitle)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(sections) { section in
Button {
selectedSection = section.features
} label : {
VCard(section: section)
}
}
}
And here is my VCard:
struct Section: Identifiable {
var id = UUID()
var title: String
var subtitle: String
var caption: String
var color: Color
var image: Image
var features: Features
}
var sections = [
Section(title: "TAB Calculations", subtitle: "Find all basic and advanced HVAC
calculations", caption: "3 sections - over 40 calculators", color: Color(hex:
"7850F0"), image: Image(systemName: "x.squareroot"), features: .calculators),
Section(title: "Upcoming Projects", subtitle: "Find upcoming and current
commissioning projects.", caption: "Over 150 projects", color: Color(hex: "6792FF"),
image: Image(systemName: "folder.fill.badge.plus"), features: .projects),
Section(title: "Valve Kv Finder", subtitle: "Quickly determine valve flow rates from
brands such as Oventropp, IMI TA and Danfoss", caption: "150 tables", color:
Color(hex: "005FE7"), image: Image(systemName: "magnifyingglass"), features:
.kvFinder)
]
enum Features: String {
case calculators
case projects
case kvFinder
}
You can use NavigationStack API if your minimum app deployment is 16+. otherwise, you may use the old NavigationView.
You can find the migration document here.
struct ContentView: View {
#State var path: [YourDestinations] = []
var body: some View {
TabView {
VStack {
NavigationStack(path: $path) { // <= here
VStack {
NavigationLink("Card 1", value: YourDestinations.place1)
NavigationLink("Card 1", value: YourDestinations.place2)
NavigationLink("Card 1", value: YourDestinations.place3)
}
.navigationDestination(for: YourDestinations.self) { destination in
switch destination {
case .place1:
Text("Detination 1")
.foregroundColor(.yellow)
case .place2:
Text("Detination 2")
.foregroundColor(.green)
case .place3:
Text("Detination 3")
.foregroundColor(.gray)
}
}
}
}
.tabItem({
Text("Tab 1")
})
Text("Hello, world!")
.padding()
.tabItem({
Text("Tab 2")
})
Text("Hello, world!")
.padding()
.tabItem({
Text("Tab 3")
})
}
}
}
I am trying to implement a tab-layout LazyVGrid that will contain three different data types. For this, I have taken a Single scrollView and have created multiple LazyVGrid to accommodate this data.
The problem I am facing is, that whenever a list from tab 1 is scrolled, the list from the tab scrolls at the same offset.
There are two different solutions I have already tried -
Create LazyVGrid as a variable - I was unable to do that since the data it will have belongs to ViewModel and also LazyVGrid is a separate view in a practical example.
Use ScrollView each time - I tried doing it but every time the currently selected type changes, SwiftUI forces LazyVGrid to repopulate and shows from offset 0.
Below is what my code looks like and it'd be great if someone could help me realize how I can fix this.
Please find my code and current output. I expect that when I switch tabs and come back, the ScrollView offset remains at the position where I left it.
import SwiftUI
enum SearchType: String, CaseIterable {
case movie = "Movies"
case tv = "TV Shows"
case people = "People"
}
struct SearchContainerView: View {
#ObservedObject var viewModel = SearchViewModel()
#State var currentSelectedSearchType: SearchType = .movie
let array: [String] = ["The", "above", "works", "great", "when", "you", "know", "where", "in", "the", "array", "the", "value" ,"is", "that", "is", "when" ,"you", "know", "its", "index", "value", "As", "the", "index", "values", "begin", "at" ,"0", "the" ,"second", "entry", "will", "be", "at", "index", "1"]
var body: some View {
NavigationView {
VStack {
HStack {
ForEach(SearchType.allCases, id: \.self) { type in
HStack {
Spacer()
Text(type.rawValue)
.font(.title3)
.onTapGesture {
self.currentSelectedSearchType = type
}
Spacer()
}
.padding(5)
.background(currentSelectedSearchType == type ? Color.gray : Color.clear)
.cornerRadius(10)
}
.background(Color.gray.opacity(0.5))
.cornerRadius(10)
.padding(.horizontal)
}
ScrollView {
switch currentSelectedSearchType {
case .movie:
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], content: {
ForEach(array, id: \.self) {
Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center)
}
})
case .tv:
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], content: {
ForEach(array, id: \.self) {
Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center)
}
})
case .people:
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], content: {
ForEach(array, id: \.self) {
Text($0).font(.largeTitle).bold().frame(width: UIScreen.main.bounds.width / 3, height: 100, alignment: .center)
}
})
}
}
}
}
}
}
Current Output -
<img src="https://i.imgur.com/TgkIXYo.mp4" alt="this slowpoke moves" width="250" />
I understand you want to switch between the different GridViews, but they should keep their individual scroll position.
To achieve that all 3 ScollViews have to stay in the view hierarchy, otherwise – as you stated – they are rebuilt and loose their position.
You can e.g. do that by putting all in a ZStack and controlling opacity (and activity) based on selection:
struct ContentView: View {
//#ObservedObject var viewModel = SearchViewModel()
#State var currentSelectedSearchType: SearchType = .movie
var body: some View {
NavigationView {
VStack {
HStack {
ForEach(SearchType.allCases, id: \.self) { type in
HStack {
Spacer()
Text(type.rawValue)
.font(.title3)
.onTapGesture {
self.currentSelectedSearchType = type
}
Spacer()
}
.padding(5)
.background(currentSelectedSearchType == type ? Color.gray : Color.gray.opacity(0.5))
.cornerRadius(10)
}
.padding(.horizontal)
}
ZStack { // here
SearchResults(type: "Movie")
.opacity(currentSelectedSearchType == .movie ? 1 : 0)
SearchResults(type: "Show")
.opacity(currentSelectedSearchType == .tv ? 1 : 0)
SearchResults(type: "Actor")
.opacity(currentSelectedSearchType == .people ? 1 : 0)
}
}
}
}
}
struct SearchResults: View {
let type: String
var body: some View {
ScrollView {
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], content: {
ForEach(0..<30) {
Text("\(type) \($0)")
.font(.title).bold()
.frame(height: 100, alignment: .center)
}
})
}
}
}
I've got the following view:
The Swift code looks like this:
struct TestView: View {
let options = [" ", "1", "2", "3", "4", "5", "6"]
#State var selectedIndex: Int = 0
var body: some View {
HStack(spacing: 0) {
Text("One")
Spacer()
Picker(selection: $selectedIndex, label: Text(options[selectedIndex])) {
ForEach(0 ..< options.count) {
Text(options[$0])
}
}
.background(Color.red)
.pickerStyle(MenuPickerStyle())
}
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
.background(Color.yellow)
}
}
When clicking on the red square, the Picker will be opened:
How can I extend the touch area of the red rectangle to also include the entire yellow area?
#DonMag's answer stopped working with iOS 15. Here's an updated answer that does work. Technically, it does not use Slider, the behavior is the same though. Instead a Menu is used.
struct PickerTestView: View {
let options = [" ", "1", "2", "3", "4", "5", "6"]
let optionNames = [" ", "One", "Two", "Three", "Four", "Five", "Six"]
#State var selectedIndex: Int = 0
var body: some View {
ZStack {
HStack(spacing: 0) {
Text(optionNames[selectedIndex])
Spacer()
}
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
.background(Color.yellow)
HStack(spacing: 0) {
Menu {
ForEach(0 ..< options.count) {
let index = $0
Button("\(options[index])") {
selectedIndex = index
}
}
} label: {
Label("", image: "")
.labelStyle(TitleOnlyLabelStyle())
.frame(maxWidth: .infinity)
}
}
}
}
}
struct PickerTestView_Previews: PreviewProvider {
static var previews: some View {
PickerTestView()
}
}
Let's see when Apple decides to break this implementation.
Not sure this is exactly what you're after, but give it a try (initial view is a "blank" yellow bar):
import SwiftUI
struct PickerTestView: View {
let options = [" ", "1", "2", "3", "4", "5", "6"]
let optionNames = [" ", "One", "Two", "Three", "Four", "Five", "Six"]
#State var selectedIndex: Int = 0
var body: some View {
ZStack {
HStack(spacing: 0) {
Text(optionNames[selectedIndex])
Spacer()
}
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
.background(Color.yellow)
HStack(spacing: 0) {
Picker(selection: $selectedIndex, label: Text(" ").frame(maxWidth: .infinity), content: {
ForEach(0 ..< options.count) {
Text(options[$0])
}
})
.pickerStyle(MenuPickerStyle())
}
}
}
}
struct PickerTestView_Previews: PreviewProvider {
static var previews: some View {
PickerTestView()
}
}
On launch:
Tap anywhere on the yellow bar:
After selecting "3":
I am attempting to layout a tableView using SwiftUI and WidgetKit and would like to achieve a similar result of that as the Apple's Notes widget.
My current implementation succeeds in laying out the view in the .systemLarge widget, but not in the .systemMedium widget. I would like to pin the view to the top of the widget, such that the header of "FAVOURITES" is visible in the .systemMedium.
struct PlacesWidgetEntryView : View {
var entry: Provider.Entry
let places = [
Place(name: "Place 1", imageName: "baseline_star_black_24pt"),
Place(name: "Place 2", imageName: "baseline_star_black_24pt"),
Place(name: "Place 3", imageName: "baseline_star_black_24pt"),
Place(name: "Place 4", imageName: "baseline_star_black_24pt"),
Place(name: "Place 5", imageName: "baseline_star_black_24pt"),
]
var body: some View {
VStack {
//Header
HStack {
Text("FAVOURITES")
.bold()
.frame(height: 8)
Spacer()
}
.padding()
.background(Color.blue)
//TableView
LazyVStack {
ForEach(places, id: \.self) { place in
PlaceRow(place: place)
}
}
Spacer()
}
}
}
struct PlaceRow: View {
let place: Place
var body: some View {
HStack {
Text(place.name)
.font(.body)
Spacer()
Image(place.imageName)
.resizable()
.frame(width: 28, height: 28, alignment: .center)
}
.padding(.horizontal)
.padding(.vertical, 4)
}
}
Implementation outcome:
The above is .systemLarge, which is good, and as per what I'm expecting.
The above is .systemMedium, which is not what I'm expecting. I would like to see "Favourites" anchored to the top of the widgetView, and potentially the tableView overflowing to the bottom.
Here is possible layout solution. Tested with Xcode 12.
var body: some View {
VStack(spacing: 0) {
HStack {
Text("FAVOURITES")
.bold()
.frame(height: 8)
Spacer()
}
.padding()
.background(Color.blue)
Color.clear
.overlay(
LazyVStack {
ForEach(places, id: \.self) { place in
PlaceRow(place: place)
}
},
alignment: .top)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
So from playing around in WidgetKit, it seems like if there are too many views in a widget, it starts to push the upper views off the screen. If you add more places to the array, you'll see the same thing happen with your large widget. What you can do is create separate views: one for the medium and one for the large widget, and for the medium one, just use 1-3 of your place objects to populate it.
You can use a switch statement in your PlacesEntryWidgetView with the widgetFamily to decide what you want to show on the view. I also slightly reduced the height of the image from 28 to 24.
struct PlacesWidgetEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily) var family
let places = [
Place(name: "Place 1", imageName: "blackStar"),
Place(name: "Place 2", imageName: "blackStar"),
Place(name: "Place 3", imageName: "blackStar"),
Place(name: "Place 4", imageName: "blackStar"),
Place(name: "Place 5", imageName: "blackStar"),
Place(name: "Place 6", imageName: "blackStar"),
Place(name: "Place 7", imageName: "blackStar"),
Place(name: "Place 8", imageName: "blackStar")
]
#ViewBuilder
var body: some View {
switch family {
case .systemMedium:
// widget can only show so many views so I only took first 3 places
WidgetView(places: Array(places.prefix(3)))
case .systemLarge:
WidgetView(places: places)
// I only have it set to show system medium so you can ignore
// the last case
case .systemSmall:
Text("")
#unknown default:
Text("")
}
}
}
struct WidgetView: View {
let places: [Place]
var body: some View {
VStack {
//Header
HStack {
Text("FAVOURITES")
.bold()
.frame(height: 8)
Spacer()
}
.padding()
.background(Color.blue)
//TableView
LazyVStack {
ForEach(places, id: \.self) { place in
PlaceRow(place: place)
}
}
Spacer()
}
}
}
struct PlaceRow: View {
let place: Place
var body: some View {
HStack {
Text(place.name)
.font(.body)
Spacer()
Image(place.imageName)
.resizable()
.frame(width: 28, height: 24, alignment: .center)
}
.padding(.horizontal)
.padding(.vertical, 4)
}
}
This is the preview:
Keep in mind, that you might want to switch between the simulators to make sure your widgets look good on all devices.
Below is my code to create a standard segmented control.
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])")
}
}
}
My question is how could I modify it to have a customized segmented control where I can have the boarder rounded along with my own colors, as it was somewhat easy to do with UIKit? Has any one done this yet.
I prefect example is the Uber eats app, when you select a restaurant you can scroll to the particular portion of the menu by selecting an option in the customized segmented control.
Included are the elements I'm looking to have customized:
* UPDATE *
Image of the final design
Is this what you are looking for?
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()
}
}
If I'm following the question aright the starting point might be something like the code below. The styling, clearly, needs a bit of attention. This has a hard-wired width for segments. To be more flexible you'd need to use a Geometry Reader to measure what was available and divide up the space.
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()
}
}
None of the above solutions worked for me as the GeometryReader returns different values once placed in a Navigation View that throws off the positioning of the active indicator in the background. I found alternate solutions, but they only worked with fixed length menu strings. Perhaps there is a simple modification to make the above code contributions work, and if so, I would be eager to read it. If you're having the same issues I was, then this may work for you instead.
Thanks to inspiration from a Reddit user "End3r117" and this SwiftWithMajid article, https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/, I was able to craft a solution. This works either inside or outside of a NavigationView and accepts menu items of various lengths.
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)
}
}
}
}