SwiftUI List inside ScrollView - ios

I want to make my List inside a ScrollView so that I can scroll List rows and headers together.
But I found that List inside ScrollView isn't working. It shows nothing.
I should use both of them.
I should use ScrollView so that I can make my header(image or text) also scrolled when I scroll the rows.
I should use List to use .ondelete() method.
my sample code is below.
#State private var numbers = [1,2,3,4,5,6,7,8,9]
var body: some View {
ScrollView {
Text("header")
List {
ForEach(numbers, id: \.self) {
Text("\($0)")
}
.onDelete { index in
// delete item
}
}
}
}
Anyone know why this happens and(or) how to fix?

It is possible but not when the List is using the full screen.
In the code example I used GeometryReader to make the list as big as possible. But you can also remove the GeometryReader and just insert a fixed dimension into .frame()
struct ContentView: View {
#State private var numbers = [1,2,3,4,5,6,7,8,9]
var body: some View {
GeometryReader { g in
ScrollView {
Text("header")
List {
ForEach(self.numbers, id: \.self) {
Text("\($0)")
}
.onDelete { index in
// delete item
}
}.frame(width: g.size.width - 5, height: g.size.height - 50, alignment: .center)
}
}
}
}

There is no need for two scrolling objects. You can also use section for this:
#State private var numbers = [1,2,3,4,5,6,7,8,9]
var body: some View {
List {
Section.init {
Text("Header")
}
ForEach(numbers, id: \.self) {
Text("\($0)")
}
.onDelete { index in
// delete item
}
}
}

Just put header inside the List, like
var body: some View {
List {
Text("Header").font(.title)
ForEach(numbers, id: \.self) {
Text("\($0)")
}
.onDelete { index in
// delete item
}
}
}

Related

Scroll to selected position in scrollview when view loads - SwiftUI

OUTLINE
I have 2 views, the first (view1) contains a HStack and an #ObservableObject. When the user selects a row from the HStack the #ObservableObject is updated to the string name of the row selected.
In view2 I have the same HStack as the first HStack in view1. This HStack observes #ObservableObject and desaturates all other rows except the one that matches the #ObservableObject.
PROBLEM
The HStack list in view2 is wider than the page so I would like to automatically scroll to the saturated/selected row when the view appears. I'm not totally sure how to use ScrollTo as it needs an integer and I am only storing/observing the string name.
VIEW 1
class selectedApplication: ObservableObject {
#Published var selectedApplication = "application1"
}
struct view1: View {
#ObservedObject var selectedOption = selectedApplication()
var applications = ["application1", "application2", "application3", "application4", "application5", "application6", "application7", "application8", "application9", "application10"]
var body: some View {
VStack{
ScrollView(.horizontal){
HStack{
ForEach(applications, id: \.self) { item in
Button(action: {
self.selectedOption.selectedApplication = item
}) {
VStack(alignment: .center){
Text(item)
}
}
}
}
}
}
}
}
View2:
struct View2: View {
#ObservedObject var application: selectedApplication
var applications = ["application1", "application2", "application3", "application4", "application5", "application6", "application7", "application8", "application9", "application10"]
var body: some View {
HStack{
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader{ scroll in
HStack{
ForEach(applications, id: \.self) { item in
Button(action: {
application.selectedApplication = item
}) {
Text(item)
.saturation(application.selectedApplication == item ? 1.0 : 0.05)
}
}
}
}
}
}
}
}
You can use ScrollViewReader with the id that's being applied in the ForEach, so you don't actually need the row index number (although that, too, is possible to get, if you needed to, either by using enumerated or searching the applications array for the index of the item.
Here's my updated code:
struct ContentView : View {
#ObservedObject var selectedOption = SelectedApplicationState()
var body: some View {
VStack {
View1(application: selectedOption)
View2(application: selectedOption)
}
}
}
class SelectedApplicationState: ObservableObject {
#Published var selectedApplication = "application1"
var applications = ["application1", "application2", "application3", "application4", "application5", "application6", "application7", "application8", "application9", "application10"]
}
struct View1: View {
#ObservedObject var application: SelectedApplicationState
var body: some View {
VStack{
ScrollView(.horizontal){
HStack{
ForEach(application.applications, id: \.self) { item in
Button(action: {
self.application.selectedApplication = item
}) {
VStack(alignment: .center){
Text(item)
}
}
}
}
}
}
}
}
struct View2: View {
#ObservedObject var application: SelectedApplicationState
var body: some View {
HStack{
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader{ scroll in
HStack{
ForEach(application.applications, id: \.self) { item in
Button(action: {
application.selectedApplication = item
}) {
Text(item)
.saturation(application.selectedApplication == item ? 1.0 : 0.05)
}
}
}.onReceive(application.$selectedApplication) { (app) in
withAnimation {
scroll.scrollTo(app, anchor: .leading)
}
}
}
}
}
}
}
How it works:
I just made a basic ContentView to show the two views, since I wasn't sure how they're laid out for you
ContentView owns the SelectedApplicationState (which was your selectionApplication ObservableObject (by the way, it is common practice to capitalize your type names -- that's why I changed the name. Plus, it was confusing to have a type and a property of that type with such a similar name) and passes it to both views.
SelectedApplicationState now holds the applications array, since it was being duplicated across views anyway
On selection in View1, selectedApplication in the ObservableObject is set, triggering onReceive in View2
There, the ScrollViewReader is told to scroll to the item with the id stored in selectedApplication, which is passed to the onReceive closure as app
In the event that these views are on separate pages, the position of View2 will still get set correctly once it is navigated to, because onReceive will fire on first load and set it to the correct position. The only requirement is passing that instance of SelectedApplicationState around.

SwiftUI main list scrollable header view without sections?

Is there a way to put a view in the list header without sections? The table views normally have a property called tableHeaderView which is a header for the whole table and has nothing to do with section headers.
I'm trying to have a list and be able to scroll it like tableHeaderView from UITableView.
struct MyView: View {
let myList: [String] = ["1", "2", "3"]
var body: some View {
VStack {
MyHeader()
List(myList, id: \.self) { element in
Text(element)
}
}
}
}
Thanks to Asperi. This is my final solution for the table header.
struct MyHeader: View {
var body: some View {
Text("Header")
}
}
struct DemoTableHeader: View {
let myList: [String] = ["1", "2", "3"]
var body: some View {
List {
MyHeader()
ForEach(myList, id: \.self) { element in
Text(element)
}
}
}
}
Here is a simple demo of possible approach, everything else (like style, sizes, alignments, backgrounds) is extendable & configurable.
Tested with Xcode 11.4 / iOS 13.4
struct MyHeader: View {
var body: some View {
Text("Header")
.padding()
.frame(maxWidth: .infinity)
}
}
struct DemoTableHeader: View {
let myList: [String] = ["1", "2", "3"]
let headerHeight = CGFloat(24)
var body: some View {
ZStack(alignment: .topLeading) {
MyHeader().zIndex(1) // << header
.frame(height: headerHeight)
List {
Color.clear // << under-header placeholder
.frame(height: headerHeight)
ForEach(myList, id: \.self) { element in
Text(element)
}
}
}
}
}

SwiftUI: How to get drag and drop reordering of items to work?

I have the following SwiftUI view:
struct ContentView: View {
#State var model: Model
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(model.events, id: \.self) { event in
CardView(event: event)
}
.onMove { indices, newOffset in
model.events.move(fromOffsets: indices, toOffset: newOffset)
}
}
}
}
}
However, it doesn't appear that the onMove closure is executing. I believe this is because all gestures are only given to the ScrollView and so the inner views don't receive the gestures.
I tried converting this view to a List, however I don't want the row separators, which in iOS 14 I believe are impossible to hide.
So, I was wondering what I need to change to get this to allow the user to drag and drop CardViews to reorder them. Thanks!
According to Apple reply in Drag and Drop to reorder Items using the new LazyGrids?
That's not built-today, though you can use the View.onDrag(_:) and
View.onDrop(...) to add support for drag and drop manually.
Do file a feedback request if you'd like to see built-in reordering
support. Thanks!
So the solution using .onDrag & .onDrop provided in my answer for actually duplicated question in https://stackoverflow.com/a/63438481/12299030
Main idea is to track drug & drop by grid card views and reorder model in drop delegate, so LazyVGrid animates subviews:
LazyVGrid(columns: model.columns, spacing: 32) {
ForEach(model.data) { d in
GridItemView(d: d)
.overlay(dragging?.id == d.id ? Color.white.opacity(0.8) : Color.clear)
.onDrag {
self.dragging = d
return NSItemProvider(object: String(d.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DragRelocateDelegate(item: d, listData: $model.data, current: $dragging))
}
}.animation(.default, value: model.data)
...
func dropEntered(info: DropInfo) {
if item != current {
let from = listData.firstIndex(of: current!)!
let to = listData.firstIndex(of: item)!
if listData[to].id != current!.id {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
As of iOS 15, .listRowSeparator(.hidden) is available.
To keep the user's order changes, an object-based property wrapper was used. In this example #StateObject was used instead of #State.
struct ContentView: View {
#StateObject var model: Model = Model()
var body: some View {
List {
ForEach(model.events, id: \.self) { event in
CardView(event: event)
.listRowSeparator(.hidden)
}
.onMove { indices, newOffset in
model.events.move(fromOffsets: indices, toOffset: newOffset)
}
}
}
}
The result looks like this:
The following variation of the above uses .listStyle(.plain) and an EditButton() for dragging and dropping the rows more easily:
struct ContentView: View {
#StateObject var model: Model = Model()
var body: some View {
VStack {
EditButton()
List {
ForEach(model.events, id: \.self) { event in
CardView(event: event)
.listRowSeparator(.hidden)
}
.onMove { indices, newOffset in
model.events.move(fromOffsets: indices, toOffset: newOffset)
}
}
.listStyle(.plain)
}
}
}
The result looks like this:
The following code was used with the above examples for testing:
class Model : ObservableObject {
#Published var events: [String]
init() { events = (1...10).map { "Event \($0)" }}
}
struct CardView: View {
var event: String
var body: some View {
Text(event)
}
}
You can use a List and hide the separators.
.onAppear {
UITableView.appearance().separatorStyle = .none
}

How to display a text message at the Center of the view when the List datasource is empty in SwiftUI?

struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
}
}
}
}
I want to display a message at the centre of the view when the list view (table view) is empty. What is the best way to achieve this in SwiftUI. Is checking for the data source count in "onAppear" and setting some sort of Bool value the correct approach ?
struct LandmarkList: View {
var body: some View {
NavigationView {
if landmarkData.count == 0 {
VStack {
Text("is empty")
} else {
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
}
}
}
}
}
I would simply try an if else statement. If landmarkdata is nil or count = 0, show a text. Else show the list.
If you want your message to have the same behavior as a list item, use ScrollView and GeometryReader.
Embed the condition state in the Group, so that the NavigationView is displayed in any case.
Don't forget to change the background color and ignore the safe area, for more resemblance to a list.
struct LandmarkList: View {
var body: some View {
NavigationView {
Group {
if landmarkData.isEmpty {
GeometryReader { geometry in
ScrollView {
Text("Your message!")
.multilineTextAlignment(.center)
.padding()
.position(x: geometry.size.width/2, y: geometry.size.height/2)
}
}
.background(Color(UIColor.systemGroupedBackground))
} else {
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
}
}
}
.ignoresSafeArea()
.navigationTitle("Title")
}
}
}
You can also use #ViewBuilder to provide an empty state view in the list, as suggested in this answer.

How show list certain row in view?

The list in the view is limited to showing columns. I want to include the column that I want in the columns shown in the view.
How should I do?
#State var showMyRow = false
var body: some view {
Vstack {
Button(“bt”) {
self.showMyRow.toggle()
}
ImageView
....
List {
ForEach
.
.
. // if showMyRow is true, view show this row
}
}
}
Above image, showMyRow is false.
After button action, showMyRow is true.
If showMyRow is true, show me at the 7row
I'm guessing that you're trying to present multiple columns horizontally on the screen?
If so, then you can put the Lists inside a Stack
HStack {
List {
Text("Hello")
}
List {
Text("There")
}
}
And if there's a lot of columns, then you can wrap the HStack inside a scroll view.
var body: some View {
ScrollView(.horizontal) {
HStack {
List {
Text("column 1")
}
List {
Text("column 2")
}
List {
Text("column 3")
}
List {
Text("column 4")
}
}
.frame(width: 1000, height: 800)
.background(Color.red)
}
}
Updated Answer:
Not sure if I understood your question perfectly, but if you just want a list that you can add and delete things from, then the easiest would be to create a separate SwiftUI file holding a struct for the row and an ObservableObject class that checks for any changes.
1) SwiftUI File with ListItem struct & ObservableObject class:
import SwiftUI
struct ListItem: Identifiable {
let id = UUID()
let title: String
}
class Items: ObservableObject {
#Published var rows = [ListItem]()
}
2) ContentView with your image view and list:
This also has a removeItems method so you can swipe any row to delete it.
struct ContentView: View {
#ObservedObject var items = Items()
var body: some View {
NavigationView{
VStack{
Image("image")
.resizable()
.frame(width: 200, height: 200)
.padding(50)
List {
ForEach(items.rows) { item in
Text(item.title)
}
.onDelete(perform: removeItems)
}
}
.navigationBarTitle("My List")
.navigationBarItems(trailing: Button(action : {
let row = ListItem(title: "New Row")
self.items.rows.append(row)
}) {
Image(systemName: "plus")
}
)
}
}
func removeItems(at offsets: IndexSet) {
items.rows.remove(atOffsets: offsets)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources