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)
}
})
}
}
}
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 just want a Bottom curve in center of my tabView but i am not able to access tabView shape property.
This is what i want.
Note:
The curve should always remain in center. And the items should swap, which is already achieved in the given code.
import SwiftUI
struct DashboardTabBarView: View {
#State private var selection: String = "home"
struct Item {
let title: String
let color: Color
let icon: String
}
#State var items = [
Item(title: "cart", color: .red, icon: "cart"),
Item(title: "home", color: .blue, icon: "house"),
Item(title: "car", color: .green, icon: "car"),
]
var body: some View {
TabView(selection: $selection) {
ForEach(items, id: \.title) { item in // << dynamically !!
item.color
.tabItem {
Image(systemName: item.icon)
Text(item.title)
}
}
}
.onChange(of: selection) { title in // << reorder with centered item
let target = 1
if var i = items.firstIndex(where: { $0.title == title }) {
if i > target {
i += 1
}
items.move(fromOffsets: IndexSet(integer: target), toOffset: i)
}
}
}
}
Ok, actually we need to solve two problems here, first - find a height of tab bar, and second - correctly align view custom view with represented selected item over standard tab bar. Everything else is mechanics.
Here is simplified demo. Tested with Xcode 14 / iOS 16.
Main part:
a possible solution for problem #1
struct TabContent<V: View>: View {
#Binding var height: CGFloat
#ViewBuilder var content: () -> V
var body: some View {
GeometryReader { gp in // << read bottom edge !!
content()
.onAppear {
height = gp.safeAreaInsets.bottom
}
.onChange(of: gp.size) { _ in
height = gp.safeAreaInsets.bottom
}
}
}
}
a possible solution for problem #2
// Just put customisation in z-ordered over TabView
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
// .. content here
}
TabSelection(height: tbHeight, item: selected)
}
struct TabSelection: View {
let height: CGFloat
let item: Item
var body: some View {
VStack {
Spacer()
Curve() // put curve over tab bar !!
.frame(maxWidth: .infinity, maxHeight: height)
.foregroundColor(item.color)
}
.ignoresSafeArea() // << push to bottom !!
.overlay(
// Draw overlay
Circle().foregroundColor(.black)
.frame(height: height).aspectRatio(contentMode: .fit)
.shadow(radius: 4)
.overlay(Image(systemName: item.icon)
.font(.title)
.foregroundColor(.white))
, alignment: .bottom)
}
}
Test module is here
I have problem with making this itemView to navigationLink. I need onTapGesture to open next list
https://github.com/reddogwow/test/blob/main/MainMenu
var objectView: some View {
VStack {
Text(objectname)
.foregroundColor(.white)
.font(.system(size: 25, weight: .medium, design: .rounded))
Image(objectphoto)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
}
.frame(height: 200)
.frame(maxWidth: .infinity)
.background(Color.blue)
}
Best edit will be where i can use Destination name from item (navMenu string)
I need something like this
var body: some View {
// NavigationView {
let columns = Array(
repeating: GridItem(.flexible(), spacing: spacing),
count: numbersOfColumns)
ScrollView {
HStack {
personView
petView
}
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(items) { item in
NavigationLink(destination: item.navMenu) {
Text("")
} label: {
ItemView(item: item)
}
}
}
.padding(.horizontal)
}
.background(Color.blue.ignoresSafeArea())
.navigationTitle("")
// }
}
Where line NavigationLink(destination: HERE MUST BE STRING TO navMenu) But now im in cycle lot of fails
I have some menus called
Menu1.swift
Menu2.swift
Menu3.swift
I need open this menu after click on Grid menu.
But destination: Must be filled with name from item in code.
struct item: Identifiable {
let id = UUID()
let title: String
let image: String
let imgColor: Color
let navMenu : String
}
item(title: "Menu 1", image: "img1", imgColor: .orange, navMenu: "Menu1"),
I thing I have bad written buy maybe only small mistake
or maybe make it like this?
var navMenuDest = destination: + item.navMenu
this will be
NavigationLink(navMenuDest) {
in finale looks like
NavigationLink(destination: Menu1)
You must have a NavigationView in the hierarchy to use NavigationLink. To make each ItemView navigate to a new view when tapped, we use NavigationLink as shown below.
Code:
struct MainMenu: View {
/* ... */
var body: some View {
NavigationView {
let columns = Array(
repeating: GridItem(.flexible(), spacing: spacing),
count: numbersOfColumns)
ScrollView {
HStack {
personView
objectView
}
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(items) { item in
NavigationLink {
Text("Some destination view here...\n\nItem: \(String(describing: item))")
} label: {
ItemView(item: item)
}
}
}
.padding(.horizontal)
}
.background(Color.blue.ignoresSafeArea())
.navigationTitle("Main Menu")
}
}
}
Result:
I am trying to make a custom segment control using a button array.
When a button is tapped, the correct value is passed to the main view and can be pressed multiple times without issue. The problem is that the previous button which was selected stays selected. The code below should work to recreate the issue. Thanks for the help.
Segment view
struct WaveTypeGridView: View {
var waveTypes = ["Beach Break" ,"Reef Break", "Point Break", "Rivermouth"]
#Binding var waveTypeSelected: String
#State var typeSelected: String
let columnSpacing: CGFloat = 5
let rowSpacing: CGFloat = 10
var gridLayout: [GridItem] {
return Array(repeating: GridItem(.flexible(), spacing: rowSpacing), count: 1)
}
var body: some View {
VStack {
Text("Wave Type \(typeSelected)")
ScrollView(.horizontal, showsIndicators: false, content: {
LazyHGrid(rows: gridLayout, alignment: .center, spacing: columnSpacing, pinnedViews: [], content: {
ForEach(waveTypes, id: \.self) { type in
if type == typeSelected {
TypeItemView(selected: true, typeSelected: self.$typeSelected, name: type)
} else {
TypeItemView(selected: false, typeSelected: self.$typeSelected, name: type)
}
}
})//: GRID
.frame(height: 18)
.padding(.vertical, 8)
.padding(.horizontal, 10)
})//: SCROLL
.background(Color(.systemGray5).cornerRadius(8))
}
}
}
struct WaveTypeGridView_Previews: PreviewProvider {
static var previews: some View {
WaveTypeGridView(waveTypeSelected: .constant(surfDataTests[0].waveType), typeSelected: "Beach Break")
.previewLayout(.sizeThatFits)
}
}
Button View
struct TypeItemView: View {
#State var selected: Bool
#Binding var typeSelected: String
let name: String
func test() {
}
var body: some View {
Button(action: {
selected.toggle()
typeSelected = name
}, label: {
HStack(alignment: .center, spacing: 6, content: {
Text(name)
.font(.footnote)
.fontWeight(.medium)
.foregroundColor(selected ? Color.white : Color.black)
})//: HSTACK
.frame(height: 18)
.padding(6)
.background(selected ? Color.blue
.cornerRadius(8)
: Color.white
.cornerRadius(8))
.background (
RoundedRectangle(cornerRadius: 8)
)
})//: BUTTON
}
}
struct TypeItemView_Previews: PreviewProvider {
static var previews: some View {
TypeItemView(selected: false, typeSelected: .constant("Rivermouth"), name:"Rivermouth")
.previewLayout(.sizeThatFits)
.padding()
.background(Color.gray)
}
}
You seem to have made your state tracking more complex than it needs to be. All you need to know is the currently selected wave type. You don't need to track a separate selected state, since the button can determine this from it's own value and the currently selected value.
For the grid view you can just have a single #State property for the selected wave type (You could inject this as an #Binding if required).
Pass the selected break type and the view's target value to the TypeItemView
struct WaveTypeGridView: View {
var waveTypes = ["Beach Break" ,"Reef Break", "Point Break", "Rivermouth"]
#State var typeSelected: String = "Beach Break"
let columnSpacing: CGFloat = 5
let rowSpacing: CGFloat = 10
var gridLayout: [GridItem] {
return Array(repeating: GridItem(.flexible(), spacing: rowSpacing), count: 1)
}
var body: some View {
VStack {
Text("Wave Type \(typeSelected)")
ScrollView(.horizontal, showsIndicators: false, content: {
LazyHGrid(rows: gridLayout, alignment: .center, spacing: columnSpacing, pinnedViews: [], content: {
ForEach(waveTypes, id: \.self) { type in
TypeItemView(typeSelected: self.$typeSelected, name: type)
}
})//: GRID
.frame(height: 18)
.padding(.vertical, 8)
.padding(.horizontal, 10)
})//: SCROLL
.background(Color(.systemGray5).cornerRadius(8))
}
}
}
Then in your TypeItemView you can create a computed property for selected based on the current value and this view's target value.
struct TypeItemView: View {
#Binding var typeSelected: String
let name: String
private var selected: Bool {
return typeSelected == name
}
func test() {
}
var body: some View {
Button(action: {
typeSelected = name
}, label: {
HStack(alignment: .center, spacing: 6, content: {
Text(name)
.font(.footnote)
.fontWeight(.medium)
.foregroundColor(self.selected ? Color.white : Color.black)
})//: HSTACK
.frame(height: 18)
.padding(6)
.background(self.selected ? Color.blue
.cornerRadius(8)
: Color.white
.cornerRadius(8))
.background (
RoundedRectangle(cornerRadius: 8)
)
})//: BUTTON
}
}
Now the whole control only depends on one #State item.
I want to change a #State var variable whenever a row in a list is being selected. Similar to what didSelectRow does in UIKit. That means when tapping on "One" the text on the right-hand side should change from "Nothing selected" to "One".
Here's my current implementation. However, tapping on a row does nothing at all.
struct ContentView: View {
#State var items = [Item(id: 1, text: "One"), Item(id: 2, text: "Two"), Item(id: 3, text: "Three")]
#State var selectedItem: Item? = nil
var body: some View {
GeometryReader { geometry in
HStack(alignment: .center) {
List(items, selection: $selectedItem) { item in
Text(item.text)
}
.frame(width: geometry.size.width / 2.5, height: geometry.size.height)
.listStyle(InsetGroupedListStyle())
Text(selectedItem?.text ?? "Nothing selected")
}
}
}
}
struct Item: Identifiable, Hashable {
var id: Int
var text: String
}
How can I change the text when someone taps a row?
Selection in List works only if EditMode is in active state, so you need to handle it manually, something like
var body: some View {
GeometryReader { geometry in
HStack(alignment: .center) {
List(items) { item in
Text(item.text)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
selectedItem = item
}
}
.frame(width: geometry.size.width / 2.5, height: geometry.size.height)
.listStyle(InsetGroupedListStyle())
Text(selectedItem?.text ?? "Nothing selected")
}
}
}
and if needed to highlight background than also do it manually.
You can use Button inside your List, and then select the Item in their action like this
List(items, id:\.self, selection: $selectedItem) { item in
Button(action: {
self.selectedItem = item
})
{
Text(item.text)
}
}