This question already has answers here:
How to scroll List programmatically in SwiftUI?
(10 answers)
Closed 9 months ago.
I have a List with 12 sections that represent the months of a year and each seach has the number of days of a month as an item. The problem is ScrollViewReader only scrolls the items of the first section. for example, January contains 31 days and it works when I call scroll.scrollTo(23, anchor: .center). How can I make scrollView scroll to a specific section 5 (June) and item 9 (today)?. Here is my code:
let daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30 ,31]
ScrollViewReader { scroll in
List {
Section("January") {
ForEach(0..<daysInMonth[0], id:\.self) {
CalendarRow(calendar: fetchCalendarData(section: 0, index: $0))
}
}
.
.
.
// and other sections to December
}
}
.onAppear {
withAnimation { scroll.scrollTo(ROW, anchor: .center) }
}
scrollTo(_:, anchor:) goes to a view with a specific ID. You aren't assigning any IDs to your views, so presumably you're accidentally taking advantage of some default. Make a specific ID for each day in each month (e.g. January-01 ... December-31) and assign that to each calendar row:
CalendarRow(...).id("June-09")
Related
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()
}
}
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.
I'm trying to implement the weekday markers in a Calendar application as shown in native Calendar app:
I have the following:
ToolbarItem(placement: .principal) {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 20) {
Text("S")
Text("M")
Text("T")
Text("W")
Text("T")
Text("F")
Text("S")
}
}
However, it doesn't span the entire width. None of the other ToolbarItemPlacement values seem to span the entire width. Is there any way I could implement this?
Is it possible to have some kind of "brick" layout in SwiftUI with a different number of items in each row? Something that would satisfy the following:
Have the system add as many item in row 1 before moving to row 2, etc.
The items length are only known at runtime
The best example I can think of is how text work, each word is different in length and only the right number of works is displayed in each row!
I searched around stack overflow and other forums / websites but haven't found anything promising yet.
Thanks for your help on this!
If I understood correctly you would need something that mimics the CollectionView:
public extension Array {
func chunked(size: Int) -> [[Element]] {
guard size > 0 else {
return [self]
}
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
and use it on a View:
ForEach(groupImages.chunked(size: rowItemCount), id: \.self) { row in
HStack(spacing: -15) {
renderThumbnailsRow(groupImages: groupImages, row: row)
Spacer()
}
}
I'm working on a calendar created with a UICollectionView.
The UICollectionView is located inside a UITableViewCell
The UITableViewCell in the heightForRowAt method return UITableView.automaticDimension
The UICollectionView used to create the calendar updates its height based on how many days are shown, for example the month of November has its first day which falls on Sunday so to place the day under the label "Sunday" the collectionView must necessarily add a whole line.
Normally each month has 5 rows (week) but when the first day of the month happens on Sunday the collectionview returns 6
Now as I said I was able to update the height of the UICollectionView based on how many rows it returns but I cannot dynamically change the height of the TableViewCell that contains the collectionView
How can I resize the tableViewCell based on the height of the CollectionView within it?
EDIT
my cell
I have set UITableView.automaticDimension for the height of the cell that contains the CalendarView
In the class CalendarView: UIView I created a variable CGFloat calendarH to set the default height of the collectionView
CollectionView implementation
I have added an observer which tracks the height of the collctionView when it changes
I am currently able to change the height of the collectionView but its superview (bookingCalendarView) and the tableView cell continue to remain with a fixed height and do not adapt to the collectionView
You need to create an #IBOutlet for your collection view's height constraint.
When you set a row's calendar / month data, determine whether you need 5 or 6 rows.
If it's 6, set the .constant on the height constraint to 750
If it's 5, set the .constant on the height constraint to 675
Edit
First, I'm going to suggest you forget about using a "self-sizing" collection view. UICollectionView is designed to lay out cells based on the size of the collection view, providing automatic scrolling when there are too many cells.
Trying to "self-size" it may work in one instance, but fail in another. The reason it fails in this case is because your table view lays out the cell and calculates its height before the collection view is populated, and thus before it can "self-size."
Instead, since you know your cell Height is 75, you can calculate how many rows your calendar will need and either set the .constant on a height constraint for your collection view, or (since you're already using heightForRowAt) calculate the row height there.
Look at this code:
let dateComponents = DateComponents(year: year, month: month)
// startDate will be the first date of the month (Jan 1, Feb 1, Mar 1, etc...)
guard let startDate = calendar.date(from: dateComponents) else {
fatalError("Something is wrong with the date!")
}
// get the range of days in the month
guard let range = calendar.range(of: .day, in: .month, for: startDate) else {
fatalError("Something is wrong with the date!")
}
// get number of days in the month
let numberOfDaysInMonth = range.count
// get the day of the week for the first date in the month
// this returns 1-based numbering
// Nov 1, 2020 was a Sunday, so this would return 1
let startDayOfWeek = Calendar.current.component(.weekday, from: startDate)
// add the "leading days to the start date"
// so, if startDayOfWeek == 3 (Tuesday)
// we need to add 2 "empty day cells" for Sunday and Monday
let totalCellsNeeded = numberOfDaysInMonth + (startDayOfWeek - 1)
// calculate number of rows needed -- this will be 4, 5 or 6
// the only time we get 4 is if Feb 1st in a non-leapYear falls on a Sunday
let numRows = Int(ceil(Double(totalCellsNeeded) / Double(7)))
// we now know the Height needed for the collection view
// you said your calendar cell height is 75, so...
// cvHeight = numRows * 75
We can put that in a loop and print() the information to the debug console like this:
override func viewDidLoad() {
super.viewDidLoad()
let calendar = Calendar.current
// 2026 is the next year where Feb starts on a Sunday
// so let's use that year to see that we get 4 rows for Feb
let year = 2026
for month in 1...12 {
let dateComponents = DateComponents(year: year, month: month)
// startDate will be the first date of the month (Jan 1, Feb 1, Mar 1, etc...)
guard let startDate = calendar.date(from: dateComponents) else {
fatalError("Something is wrong with the date!")
}
// get the range of days in the month
guard let range = calendar.range(of: .day, in: .month, for: startDate) else {
fatalError("Something is wrong with the date!")
}
// get number of days in the month
let numberOfDaysInMonth = range.count
// get the day of the week for the first date in the month
// this returns 1-based numbering
// Nov 1, 2020 was a Sunday, so this would return 1
let startDayOfWeek = Calendar.current.component(.weekday, from: startDate)
// add the "leading days to the start date"
// so, if startDayOfWeek == 3 (Tuesday)
// we need to add 2 "empty day cells" for Sunday and Monday
let totalCellsNeeded = numberOfDaysInMonth + (startDayOfWeek - 1)
// calculate number of rows needed -- this will be 4, 5 or 6
// the only time we get 4 is if Feb 1st in a non-leapYear falls on a Sunday
let numRows = Int(ceil(Double(totalCellsNeeded) / Double(7)))
// we now know the Height needed for the collection view
// you said your calendar cell height is 75, so...
// cvHeight = numRows * 75
// debug output
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEEE"
let dayName = dateFormatter.string(from: startDate)
dateFormatter.dateFormat = "LLLL y"
let dateString = dateFormatter.string(from: startDate)
let dayPadded = dayName.padding(toLength: 10, withPad: " ", startingAt: 0)
let datePadded = dateString.padding(toLength: 16, withPad: " ", startingAt: 0)
print("\(datePadded) has \(numberOfDaysInMonth) days, starting on \(dayPadded) requiring \(numRows) rows")
}
}
Here's the output:
January 2026 has 31 days, starting on Thursday requiring 5 rows
February 2026 has 28 days, starting on Sunday requiring 4 rows
March 2026 has 31 days, starting on Sunday requiring 5 rows
April 2026 has 30 days, starting on Wednesday requiring 5 rows
May 2026 has 31 days, starting on Friday requiring 6 rows
June 2026 has 30 days, starting on Monday requiring 5 rows
July 2026 has 31 days, starting on Wednesday requiring 5 rows
August 2026 has 31 days, starting on Saturday requiring 6 rows
September 2026 has 30 days, starting on Tuesday requiring 5 rows
October 2026 has 31 days, starting on Thursday requiring 5 rows
November 2026 has 30 days, starting on Sunday requiring 5 rows
December 2026 has 31 days, starting on Tuesday requiring 5 rows
So... either in:
cellForRowAt ... calculate the needed height and set the CV height in the cell, or
heightForRowAt ... calculate and return the needed height for the row
Side note: I'd suggest using auto-layout for all of your cells, instead of returning various row heights.