SwiftUI Jump Menu (sectionIndexTitles) - ios

I have an alphabetically sorted SwiftUI List with sections. I try to set a jump menu / sidebar for scrolling through the sections like here:
In UIKit it was possible by setting the sectionIndexTitles. But I couldn't find a solution for SwiftUI.

What you can do in SwiftUI 2.0 is to use the ScrollViewReader.
First, in your list, each element has to have a unique ID which can be any hashable object. Simply use the .id modifier. For example:
List(0..<10) { i in
Text("Row \(i)")
.id(i)
}
After that you can use the ScrollViewReader as following:
ScrollViewReader { scroll in
VStack {
Button("Jump to row #10") {
scroll.scrollTo(10)
}
List(0..<10) { i in
Text("Row \(i)")
.id(i)
}
}
}
So in your case you could give the each alphabetical section an id, so section "a" would have .id(a) etc. After that you could use the sidebar you've implemented and jump to the desired alphabetical section inside ScrollViewReader.
Edit: So I have tried to make a very simple solution quickly. It is not perfect but it serves your purpose. Feel free to copy and modify the code:
struct AlphaScroller: View {
let alphabet = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"]
let sampleItems = ["Apple", "Banana", "Orange"]
var body: some View {
ScrollView {
ScrollViewReader{ scroll in
VStack {
//top character picker with horizontal Scrolling
ScrollView(.horizontal) {
HStack(spacing: 20) {
ForEach(alphabet, id: \.self) { char in
Button(char){
withAnimation {
scroll.scrollTo(char, anchor: .top)
}
}
}
}
}.frame(width: UIScreen.main.bounds.width * 0.8, height: 20)
//list of sections
ForEach(alphabet, id: \.self){ char in
HStack {
VStack {
Text(char).id(char)
.font(.system(size: 30))
.padding()
ForEach(sampleItems, id: \.self){ item in
Text(item)
.padding()
}
Rectangle()
.foregroundColor(.gray)
.frame(width: UIScreen.main.bounds.width, height: 1)
}
Spacer()
}
}
}
}
}
}
}

Related

SwiftUI Picker in Form - Can't Dynamically Size the Form Space

I'm struggling with a view where I want to have multiple pickers embedded in
other views. When I wrap the pickers in a Form, I get the desired behavior for the
picker but there is a lot of extra space around the pickers that I can't seem to
automatically adjust.
This is an example - the space in the red outline seems to be determined by the other
view elements not the size of the picker.
I can, of course, hard-code a frame height for the Form but that is trial and error
and would only be specific to the device and orientation. I have tried multiple
versions of Stacks inside Stacks with padding, GeometryReader etc, but I have not come up with any
solution. As an aside, I DO want the picker labels, otherwise I could just remove
the Form.
I also tried setting UITableView.appearance().tableFooterView in an init() but that did not work either.
Here is a simplified version:
struct ContentView4: View {
#State var selectedNumber1: Int = 1
#State var selectedNumber2: Int = 2
#State var selectedNumber3: Int = 3
var body: some View {
NavigationView {
VStack(alignment: .leading) {
HStack {
Spacer()
Text("Compare up to 3")
.font(.caption)
Spacer()
}//h
Form {//for pickers
Picker(selection: $selectedNumber1, label: Text("A")) {
ForEach(0..<10) {
Text("\($0)")
}
}//picker
Picker(selection: $selectedNumber2, label: Text("B")) {
ForEach(0..<10) {
Text("\($0)")
}
}//picker
Picker(selection: $selectedNumber3, label: Text("C")) {
ForEach(0..<10) {
Text("\($0)")
}
}//picker
}//form for pickers
.padding(.horizontal, 10)
//.frame(height: 200) //don't want to hard code this
VStack(alignment: .leading) {
HStack {
Text("A")
.frame(width: 100)
Text("B")
.frame(width: 100)
Text("C")
.frame(width: 100)
}
.padding(.horizontal, 10)
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading){
Text("A title line")
.font(.headline)
.padding(.vertical, 5)
HStack {
Text("Number")
.frame(width: 100)
Text("Number")
.frame(width: 100)
Text("Number")
.frame(width: 100)
}
Text("Another title line")
.font(.headline)
.padding(.vertical, 5)
HStack {
Text("Something")
.frame(width: 100)
Text("Something")
.frame(width: 100)
Text("Something")
.frame(width: 100)
}
Text("A Third title line")
.font(.headline)
.padding(.vertical, 5)
HStack {
Text("More")
.frame(width: 100)
Text("More")
.frame(width: 100)
Text("More")
.frame(width: 100)
}
}
}//scroll
.padding(.horizontal, 10)
}
.navigationBarTitle("Compare Three", displayMode: .inline)
}
}//nav
}//body
}//struct
Interestingly, I am able to get a solution by removing the form and wrapping each
picker in a menu, like this:
Menu {
Picker(selection: $selectedNumber2, label: EmptyView()) {
ForEach(0..<10) {
Text("\($0)")
}
}//picker
} label: {
HStack {
Text("B")
Spacer()
Image(systemName: "chevron.right")
.resizable()
.frame(width: 14, height: 14)
}//h
}//menu label
However, I still like the look of the Form better if I could automatically configure
the space around the Form items.
Any guidance would be appreciated. Xcode 13.4, iOS 15.5
Form (and List) is not meant to be stacked inside other views like this, which is why it has such strange behavior.
Thankfully, it's fairly simple to recreate the stuff you do want using NavigationLink. Here’s a quick example of a couple custom views that do just that:
// drop-in NavigationLink replacement for Picker
struct NavigationButton<Content: View, SelectionValue: Hashable> : View {
#Binding var selection: SelectionValue
#ViewBuilder let content: () -> Content
#ViewBuilder let label: () -> Text
var body: some View {
NavigationLink {
PickerView(selection: $selection, content: content, label: label)
} label: {
HStack {
label()
Spacer()
Text(String(describing: selection))
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(NavigationLinkButtonStyle())
}
}
// subview for the Picker page, which lets us use `dismiss()`
// to pop the subview when the user selects an option
struct PickerView<Content: View, SelectionValue: Hashable> : View {
#Binding var selection: SelectionValue
#ViewBuilder let content: () -> Content
#ViewBuilder let label: () -> Text
#Environment(\.dismiss) private var dismiss
var body: some View {
Form {
Picker(selection: $selection, content: content, label: label)
.pickerStyle(.inline)
.labelsHidden()
.onChange(of: selection) { _ in
dismiss()
}
}
.navigationTitle(label())
}
}
// recreate the appearance of a List row
struct NavigationLinkButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.label
.frame(maxWidth: .infinity)
Image(systemName: "chevron.right")
.font(.footnote.bold())
.foregroundColor(Color(UIColor.tertiaryLabel))
}
.padding()
.background(
Rectangle()
.fill(configuration.isPressed ? Color(UIColor.quaternaryLabel) : Color(UIColor.systemBackground))
)
}
}
If you like the .insetGrouped style you got using Form, we can replicate that by putting NavigationButton inside a clipped VStack:
VStack(spacing: 0) {
NavigationButton(selection: $selectedNumber1) {
ForEach(0..<10) {
Text("\($0)")
}
} label: {
Text("A")
}
Divider()
NavigationButton(selection: $selectedNumber2) {
ForEach(0..<10) {
Text("\($0)")
}
} label: {
Text("B")
}
}
.clipShape(RoundedRectangle(cornerRadius: 11))
.padding()
.background(Color(UIColor.systemGroupedBackground))
And here’s a screenshot showing my custom views above your original Form.
(And if you like Picker as a popup menu, you could use Menu instead of NavigationLink)

Lazy loading SwiftUI Grid on both vertical and horizontal direction

As the title says I try to build a Grid view that loads lazily on both vertical and horizontal, the main goal is building a calendar for a list of employee, both the list and the calendar interval can be very large so they have to be rendered lazily. I've tried LazyHGrid and LazyVGrid but they only render views lazily on one direction. The further approach uses one LazyVStack for the whole view and one LazyHStack for each row, the loading is lazily (check print in the console) but while scrolling the views loses their position and the grid get's broken
struct ContentView: View {
var body: some View {
ScrollView([.horizontal, .vertical]) {
LazyVStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: columnHeaders) {
ForEach(0..<20) { row in
LazyHStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: rowHeader(with: "Employee \(row)")) {
ForEach(0..<100) { column in
Text("Cell \(row), \(column)")
.foregroundColor(.white)
.font(.largeTitle)
.frame(width: 200, height: 100)
.background(Color.red)
.onAppear {
print("Cell \(row), \(column)")
}
}
}
}
}
}
}
}
}
var columnHeaders: some View {
LazyHStack(spacing: 20, pinnedViews: .sectionHeaders) {
Section(header: rowHeader(with: "")) {
ForEach(0..<100) { column in
Text("Header \(column)")
.frame(width: 200, height: 100)
.background(Color.white)
}
}
}
}
func rowHeader(with label: String) -> some View {
Text(label)
.frame(width: 100, height: 100)
.background(Color.white)
}
}
PS: I also need fixed headers, that is achieved in the code above using pinnedViews
You can fix the out-of-alignment problem by using a pair of nested single-axis ScrollViews instead of one multi-axis ScrollView.

Swift UI navigationLink from item

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:

Add contextMenu in the items inside of a List in SwiftUI

I have created a photo library, there is list with many cells and in each cells. there are a few UIImages.
I added contextMenu the images, but when I long press on each image, the entire cell will be called instead of each image.
could anyone help me to how to add contextMenu to each the items inside of a List
struct PhotoList : View {
var photoLibrary = PhotoLibrary
var body : some View {
GeometryReader { geometry in
List(self.photoLibrary, id: \.self) { imageSet in
HStack (alignment: .center) {
ForEach(imageSet, id: \.self) { image in
Image(uiImage: image)
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
VStack {
Button(action: {}) {
HStack {
Text("Add to Today")
Image("plus.circle")
}
}
}
}
}
}
}
}
}
}
The problem with this is that the list view will force touch events to be registered to the first child view. For example it is not possible to create a list with multiple buttons in each row as in the following code.
// Individual buttons can't be pressed
List(someList, id: \.self) { _ in
HStack {
ForEach(1..<4) { k in
Button("Button") {
//what ever
}
}
}
}
HStack will get all the touch events....
Though it is possible to solve your problem by using a combination of a scrollview and a vstack. If scrolling isn't necessary just remove that piece of code.
var body : some View {
GeometryReader() { geometry in
ScrollView {
VStack {
List(self.photoLibrary, id: \.self) { imageSet in
HStack (alignment: .center) {
ForEach(imageSet, id: \.self) { image in
Image(uiImage: image)
.resizable()
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
VStack {
Button(action: {}) {
HStack {
Text("Add to Today")
Image("plus.circle")
}
}
}
}
}
}
}
}
}.position(x: geometry.size.width/2, y: geometry.size.height/2)
}
}
Code needs to be corrected for your design though. Added .resizable to the image as well. Hope this can be a start for your code.
Replace your List with a ForEach and wrap it in a VStack or LazyVStack and you're good to go. If you need scrolling wrap it in a ScrollView. The List makes it behave like table rows.
So instead of
List(self.photoLibrary, id: \.self) { imageSet in
// ...
}
do this:
ScrollView { // If scrolling is needed
VStack {
ForEach(self.photoLibrary, id: \.self) { imageSet in
// ...
}
}
}
Pretty much as NoosaMaan explained in more detail:
"[…] the list view will force touch events to be registered to the first child view […]"
I'm not sure what your struct for photoLibrary is like, so I created my own. This code will need to be changed slightly depending on your struct.
This ContentView can be called passing a list of photoLibrary to display on the screen.
struct photoLibrary {
var imageSet: [ImageItem]
init(){
self.imageSet = [ImageItem()]
}
}
struct ImageItem{
var image1: Image
var image2: Image
var image3: Image
init(){
self.image1 = Image(systemName: "xmark")
self.image2 = Image(systemName: "xmark")
self.image3 = Image(systemName: "xmark")
}
}
struct ContentView: View {
#State var photoLib: [photoLibrary]
var body: some View {
VStack {
ScrollView{
ForEach(self.photoLib.indices, id: \.self) { item in
VStack{
ForEach(photoLib[item].imageSet.indices, id: \.self) { image in
HStack{
ImageView(image: photoLib[item].imageSet[image].image1)
ImageView(image: photoLib[item].imageSet[image].image2)
ImageView(image: photoLib[item].imageSet[image].image3)
}
}
}
}
}
}
}
struct ImageView: View{
#State var image: Image
var body: some View{
image
.resizable()
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
Text("Add to Today").onTapGesture {
//do something
}
Image(systemName: "plus.circle").onTapGesture {
//do something
}
}
}
}
Display example with xmarks as the image:
As I know a context menu has to have at least two buttons but the following way I think it would be enough:
Image(uiImage: image)
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
Button(action: {}) {
Text("Add to Today")
Image("plus.circle")
}
Button(action: {}) {
Text("Some Other Button")
Image("globe")
}
}

How to make a Horizontal List in SwiftUI?

I can wrap all my views inside a List
List {
// contents
}
But this seems to be vertical scrolling. How do I make it horizontal?
You need to add .horizontal property to the scrollview. otherwise it won't scroll.
ScrollView (.horizontal, showsIndicators: false) {
HStack {
//contents
}
}.frame(height: 100)
Starting from iOS 14 beta1 & XCode 12 beta1 you will be able to wrap LazyHStack in a ScrollView to create a horizontal lazy list of items, i.e., each item will only be loaded on demand:
ScrollView(.horizontal) {
LazyHStack {
ForEach(0...50, id: \.self) { index in
Text(String(index))
.onAppear {
print(index)
}
}
}
}
To make a horizontal scrollable content, you can wrap a HStack inside a ScrollView:
ScrollView {
HStack {
ForEach(0..<10) { i in
Text("Item \(i)")
Divider()
}
}
}
.frame(height: 40)
You can use .horizontal property and use custom elements. For me I use cusomt CircleView
var body: some View {
VStack{
Divider()
ScrollView(.horizontal){
HStack(spacing:10){
ForEach(0..<10){
index in
CircleView(label: "\(index)")
}
}.padding()
}.frame(height:100)
Divider()
Spacer()
}
}
}
//struct CircleView:View
#State var label:String
var body:some View {
ZStack{
Circle()
.fill(Color.yellow)
.frame(width: 70, height: 70)
Text(label)
}
}
}

Resources