Full width Toolbar Item in SwiftUI - ios

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?

Related

SwiftUI: Scrollview with both axes enabled produces layout mess

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/

SwiftUI: LazyVGrid, GridItem filling width

I am trying to create a grid with a known number of columns. For this example the number of columns is 3.
I would like the first column to fill the space available, but it looks like it only fills 1/3 of the space available.
struct ContentView: View {
var body: some View {
LazyVGrid(columns: self.columns, alignment: .leading, spacing: 10) {
Group {
Text("Fill")
.frame(maxWidth: .infinity)
Text("Adapt")
Text("Adapt")
}
.background(Color.red)
}
}
var columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
}
Result:
But I would like the first column to fill all the available space, while the last 2 columns are pushed to the right side.
Is this possible with a LazyVGrid?. Of course without having to specify frame sizes manually. Thanks!

How to make infinite ScrollView with infinite amount of items in SwiftUI

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()
}
}

Content width of TabView in SwiftUI

I'm trying to add some content insets in TabView with page style. Is it possible to change TabView content width, so it could show a part of next and previous View? I can easily do it with HStack or ScrollView, but I wonder if I can do it with TabView as well, because TabView has many features I need in this case.
My test code looks like this:
var body: some View {
TabView {
Group {
ExcursionView(index: 1, count: 3)
ExcursionView(index: 2, count: 3)
ExcursionView(index: 3, count: 3)
}
.padding(.horizontal, 20)
}
.tabViewStyle(.page)
}

SwiftUI alignment of "rating" views in two separate rows in Form for macOS and iOS

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.

Resources