I'm new to IOS and SwiftUI so please correct any incorrect assumptions I make.
I need to make a View in SwiftUI that has similar functionality to RecyclerView in android.
Specifically what I'm trying to make is a horizontal ScrollView where the displayed item (the one in the centre of the screen) is constructed by its index.
for example If I'm looking at index 0 then it will display a card with some date text in ie (1st Jan 2022)
then index 1 will display 2nd jan 2022, index 2 3rd jan 2022 ect...
So you can see it could theoretically go on forever if the user wants to scroll far enough.
Because of this I can't create a list of objects and loop over them in a HStack. So I want some kind of functionality where I only create 3 cards and then when i scroll to the right and the index 0 card goes out of sight, it is reused as the 'new' card at the right (end) of the ScrollView. The same behaviour as the RecyclerView.
The most important thing is I need to be able to access the index for a particular rendered card, without that i can't construct my data.
You can use LazyHGrid to achieve RecyclerView functionality
struct ContentView: View {
// MARK: - PROPERTIES
let gridItems = [
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View{
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: gridItems, alignment: .center, spacing: 10) {
ForEach(0...10, id: \.self){ number in
// You can create your own ReusableView and put it here
Text("\(number)")
.frame(width: 100)
.background(.blue)
}
}
}
.frame(height: 100)
}
}
// MARK: - PREVIEW
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Related
This post goes into great detail of SwiftUI's new Grid view in iOS 16, however it uses static examples. I've created a demo project below to show an example, in my use case I do not know the exact number or length of the strings in data. Is it possible to use this new Grid API to layout these views in a way that will evenly distribute them, i.e. construct my code so that, e.g. if 3 view's do not fit, it will only make 2 columns and push the 3rd view to the next row so that they won't overlap?
import SwiftUI
struct ContentView: View {
var data = [
"Beatles",
"Pearl Jam",
"REM",
"Guns n Roses",
"Red Hot Chili Peppers",
"No Doubt",
"Nirvana",
"Tom Petty and the Heart Breakers",
"The Eagles"
]
var body: some View {
Grid() {
GridRow {
ForEach(data, id: \.self) { bandName in
Text(bandName)
.fixedSize(horizontal: true, vertical: false)
}
}
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The Problem
In our brand new SwiftUI project (Multiplatform app for iPhone, iPad and Mac) we are using a ScrollView with both .horizontal and .vertical axes enabled. In the end it all looks like a spreadsheet.
Inside of ScrollView we are using LazyVStack with pinnedViews: [.sectionHeaders, .sectionFooters]. All Footer, Header and the content cells are wrapped into a LazyHStack each. The lazy stacks are needed due to performance reasones when rows and columns are growing.
And this exactly seems to be the problem. ScrollView produces a real layout mess when scrolling in both axes at the same time.
Tested Remedies
I tried several workarounds with no success.
We built our own lazy loading horizontal stack view which shows only the cells whose indexes are in the visible frame of the scrollview, and which sets dynamic calculated leading and trailing padding.
I tried the Introspect for SwiftUI packacke to set usesPredominantAxisScrolling for macOS and isDirectionalLockEnabled for iOS. None of this works as expected.
I also tried a workaround to manually lock one axis when the other one is scrolling. This works fine on iOS but it doesn't on macOS due to extreme poor performance (I think it's caused by the missing NSScrollViewDelegate and scroll events are being propagated via NotificationCenter).
How does it look?
To give you a better idea of what I mean, I have screenshots for both iOS and macOS.
Here you can watch short screen recording for both iOS and macOS.
Example Code
And this is the sample code used for the screenshots. So you can just create a new Multiplatform project and paste the code below in the ContentView.swift file. That's all.
import SwiftUI
let numberOfColums: Int = 150
let numberOfRows: Int = 350
struct ContentView: View {
var items: [[Item]] = {
var items: [[Item]] = [[]]
for rowIndex in 0..<numberOfRows {
var row: [Item] = []
for columnIndex in 0..<numberOfColums {
row.append(Item(column: columnIndex, row: rowIndex))
}
items.append(row)
}
return items
}()
var body: some View {
Group {
ScrollView([.horizontal, .vertical]) {
LazyVStack(alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders, .sectionFooters]) {
Section(header: Header()) {
ForEach(0..<items.count, id: \.self) { rowIndex in
let row = items[rowIndex]
LazyHStack(spacing: 1) {
ForEach(0..<row.count, id: \.self) { columnIndex in
Text("\(columnIndex):\(rowIndex)")
.frame(width: 100)
.padding()
.background(Color.gray.opacity(0.2))
}
}
}
}
}
}
.edgesIgnoringSafeArea([.top])
}
.padding(.top, 1)
}
}
struct Header: View {
var body: some View {
LazyHStack(spacing: 1) {
ForEach(0..<numberOfColums, id: \.self) { idx in
Text("Col \(idx)")
.frame(width: 100)
.padding()
.background(Color.gray)
}
Spacer()
}
}
}
struct Item: Hashable {
let id: String = UUID().uuidString
var column: Int
var row: Int
}
Can You Help?
Okay, long story short...
Does anybody know a real working solution for this issue?
Is it a known SwiftUI ScrollView bug?
If there were a way to lock one scrolling direction while the other one is being scrolled, then I could deal with that. But at the moment, unfortunately, it is not usable for us.
So any solution is highly appreciated! 😃
Is there any reason you don't want to use a Grid for this layout?
Here is the article I would suggest you read to solve this problem:
https://swiftui-lab.com/impossible-grids/
I am looking for how to align items in a list horizontally, where the items start at the last item, or on the right edge, and go left from there.
For example, with a list of 1-20, I'd be looking to show items 1-5 off the left edge of the screen, with item 20 being the right-most item, right on the edge of the screen. An image of this can be shown below (it is not the solution running, but rather a contrived image to show the desired effect).
How can I do this in SwiftUI?
An HStack will simply center itself horizontally. HStack doesn't seem to have a horizontal alignment that I can control.
An HStack in a scrollview allows me to reach the right-most item, but still starts at the first item, or the left-most edge of the list.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Divider()
// the HStack just tries to fit everything within the width of the view,
// without anything going "offscreen"
HStack(spacing: 10) {
ForEach(1..<21) { index in
Text("\(index)")
}
Spacer()
}
Divider()
// placing the HStack within a ScrollView does allow elements to go offscreen,
// but the list starts at 1 and goes offscreen at 16 on the right
// I am looking for how to "anchor" the list on the right edge instead,
// with elements 1 through 5 being displayed off the left edge of the screen,
// and the right-most element being 20, directly on the right edge of the screen
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(1..<21) { index in
Text("\(index)")
}
}
}
.frame(height: 100)
Divider()
// are there any other ways to achieve my desired effect?
}
}
}
If I properly understand question looks like you need ScrollViewReader:
// Define array for your label
var entries = Array(0...20)
ScrollView(.horizontal) {
ScrollViewReader { reader in
HStack(spacing: 10) {
ForEach(entries, id: \.self) { index in
Text("\(index)")
}.onAppear {
// Here on appear scroll go to last element
reader.scrollTo(entries.count - 1)
}
}
}
}
Writing a Multiplatform app initially for macOS and iOS that uses a master detail view structure and within the detail view is a Form and many Sections, one of which contains two structs that represent a user's rating (of food) for enjoyment and consumption.
Here is the code for the enjoyment rating...
struct Rating: View {
#Environment(\.colorScheme) var colourScheme
#Binding var rating: Int
var body: some View {
HStack {
Text("Enjoyment")
.padding(.trailing, 12.0)
#if os(iOS)
Spacer()
#endif
ForEach(0..<5) { counter in
Image(systemName: rating > counter ? "star.fill" : "star")
.onTapGesture(count: 1) {
if rating == 1 {
rating = 0
}
else {
rating = counter + 1
}
}
// this line for image system name = "circle.fill" : "circle"
//.padding(.trailing, counter != 4 ? 2.5 : 0)
}
.shadow(color: colourScheme == .light ? .gray : .white, radius: colourScheme == .light ? 1.0 : 2.0, x: 0, y: 0)
}
.foregroundColor(Color.accentColor)
}
}
The consumption rating is very similar with minor changes.
So far these look good on iOS because the (iOS only) Spacer() pushes each Rating view to the right or trailing edge of the row and my .padding modifier hack for the circle image makes the spacing between each image "about right".
While I'm struggling to figure out how to align the "dots" for the macOS target, I'm also struggling to figure out how to align each image programmatically, so that if I changed the image the alignment would work.
See screenshots below (that illustrate how the five Images do not align).
iOS
macOS
I've read a few blogs on the .alignmentGuide modifier including Alignment guides in SwiftUI by Majid Jabrayilov.
It seems to be the way I should go but I'm stuck in my attempts on how to work this out.
I've added a comment about the use of Spacer() between your label and your rating control, but separately it's worth looking at the rating control itself.
Firstly, right now you're relying on the five elements sharing an HStack with the label, and using conditional padding logic within the loop to control the spacing between elements.
That part would be easier if you give your rating element its own HStack. That way, spacing between elements can be determined using the stack's spacing attribute without having to worry about whether or not you're on the last loop iteration. For example:
HStack(spacing: 2.5) {
ForEach(0..<5) {
Image(systemName: "circle")
// etc.
}
}
In terms of aligning the child elements of a rating view so that they align with a similar view below regardless of the symbol being used, you can constrain the frame width of each child element to be the same, regardless of what image they're displaying.
You can accomplish that by adding a .frame() modifier to each child element in the loop:
HStack {
ForEach(0..<5) {
Image(systemName: "xxx")
.frame(width: 40, alignment: .center)
}
}
You'd obviously need to pick a width that works for you - and you could mix this with a spacing attribute on the HStack as well.
There is an unexpected jump in the ScrollView when used in a NavigationView with large title.
The inconsistent behaviour can be seen below:
The minimum code required to reproduce:
import SwiftUI
struct LeaderboardView: View {
var body: some View {
NavigationView(){
ScrollView(){
VStack(){
ForEach(0...50, id: \.self){ number in
Text("Number: \(number)").padding()
}
}.frame(maxWidth:.infinity)
}.navigationBarTitle("Numbers")
Spacer();
}
}
}
struct LeaderboardView_Previews: PreviewProvider {
static var previews: some View {
LeaderboardView()
}
}
The weirder thing is that after I add vertical padding to the scroll view, the behaviour changes and it does not snap as fast as in the photo above, but it is still un-natural. This can also be reproduced in canvas while previewing.
The expected behaviour would be something like this:
Even more on point would be the Settings page in WhatsApp.
I'm a beginner with Swift(UI). Is there a way to work around this? What am I missing?
Note: I am using XCode 12 beta 3
To have behaviour like on second gif use List
NavigationView(){
List{
ForEach(0...50, id: \.self){ number in
Text("Number: \(number)").padding()
}
}.navigationBarTitle("Numbers")
}
This issue has been solved with the Xcode Beta 4 and iOS 14 Beta 4 update.
The NavigationView() with ScrollView() can be used with no problems.