HStack, List, ForEach of VStack not working - swiftui

I have the following view:
var body: some View {
HStack(
alignment: .top
) {
// List {
ForEach(taskLists, id: \.self) { list in
VStack(alignment: .leading) {
TaskListView(list: list)
.padding(10)
}
}
.onMove { indices, newOffset in
print("mooooooooove")
}
}
//}
}
So, it is a horizontal stack of several VStacks. This works fine (e.g):
■ ■ ■
Now, I want to wrap ForEach with the List to enable the drag-n-drop. When I uncomment the lines in the above code, the view changes to the vertical stack of the inner VStacks, i.e. they are now one above the other:
■
■
■
How can I use List&ForEach and remain the horizontal stack AND to enable the onMove()?
Please note that putting List before HStack gives the correct view, but the method onMove is not being called.

Related

SwiftUI Scrollable Charts in IOS16

Using the new SwiftUI Charts framework, we can make a chart bigger than the visible screen and place it into a ScrollView to make it scrollable. Something like this:
var body : some View {
GeometryReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
Chart {
ForEach(data) { entry in
// ...
}
}
.frame(width: proxy.size.width * 2)
}
}
}
Does anybody know if it is possible to programmatically move the scroll to display a certain area of the chart?
I've tried using ScrollViewReader, setting the IDs at the x-axis labels, and trying to use the scrollTo function to navigate to any of those positions with no luck:
Chart {
/// ...
}
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { value in
if let date : Date = value.as(Date.self) {
Text(date, style: .date)
.font(.footnote)
}
}
}
This cheesy workaround seems to do the trick. I put the chart in a ZStack with an HStack overlaying the chart. The HStack contains a bunch of invisible objects that conform to the Identifiable protocol. The quantity, ids, and positions of the invisible objects match the charted data.
Since the ZStack view now contains identifiable elements, ScrollViewReader works as expected.
import SwiftUI
import Charts
struct ChartData: Identifiable {
var day: Int
var value: Int
var id: String { "\(day)" }
}
struct ContentView: View {
#State var chartData = [ChartData]()
#State var scrollSpot = ""
let items = 200
let itemWidth: CGFloat = 30
var body: some View {
VStack {
ScrollViewReader { scrollPosition in
ScrollView(.horizontal) {
// Create a ZStack with an HStack overlaying the chart.
// The HStack consists of invisible items that conform to the
// identifible protocol to provide positions for programmatic
// scrolling to the named location.
ZStack {
// Create an invisible rectangle for each x axis data point
// in the chart.
HStack(spacing: 0) {
ForEach(chartData) { item in
Rectangle()
.fill(.clear)
// Setting maxWidth to .infinity here, combined
// with spacing:0 above, makes the rectangles
// expand to fill the frame specified for the
// chart below.
.frame(maxWidth: .infinity, maxHeight: 0)
// Here, set the rectangle's id to match the
// charted data.
.id(item.id)
}
}
Chart(chartData) {
BarMark(x: .value("Day", $0.day),
y: .value("Amount", $0.value),
width: 20)
}
.frame(width: CGFloat(items) * itemWidth, height: 300)
}
}
.padding()
.onChange(of: scrollSpot, perform: {x in
if (!x.isEmpty) {
scrollPosition.scrollTo(x)
scrollSpot = ""
}
})
}
.onAppear(perform: populateChart)
Button("Scroll") {
if let x = chartData.last?.id {
print("Scrolling to item \(x)")
scrollSpot = x
}
}
Spacer()
}
}
func populateChart() {
if !chartData.isEmpty { return }
for i in 0..<items {
chartData.append(ChartData(day: i, value: (i % 10) + 2))
}
}
}
IMHO this should work out of the SwiftUI box. Apple's comments for the initializer say it creates a chart composed of a series of identifiable marks. So... if the marks are identifiable, it is not a stretch to expect ScrollViewReader to work with the chart's marks.
But noooooo!
One would hope this is an oversight on Apple's part since the framework is new, and they will expose ids for chart marks in an upcoming release.

How to make the space between views grow when given a large frame, but be as small as possible otherwise?

This question is essentially about how to define layout behaviour for a SwiftUI View such that it grows/shrinks in a particular way when given different frames externally. IE imagine you are creating a View which will be packaged up in a library and given to somebody else, without you knowing how much space they will give to your view.
The layout I would like to create will contain two horizontal views, indicated by A & B in my diagrams. I would like to control how this view expands if you specify a frame like follows:
When no frame is specified, I'd like my container View to be as small as the inner views and no bigger. See diagram 1.
When the container View is given a frame that's larger than the inner views, I'd like the space between the inner views to grow. See diagram 2.
Diagram 1: How I'd like my View to look without a frame specified.
// MyView()
| [A B] |
Diagram 2: How I'd like my View to look with a large frame.
// MyView().frame(maxWidth: .infinity)
|[A B]|
Diagram Key:
| represents my Window
[] represents my container View
A and B are my child Views.
My naive attempts:
Unmodified HStack
The behaviour of an unmodified HStack matches Diagram 1 with an unspecified frame successfully, however when given a large frame it's default behaviour is to grow as follows:
// HStack{A B}.frame(maxWidth: .infinity)
|[ AB ]|
HStack with a Spacer between the views
If I use a Stack with but add a spacer in between the views, the spacer grows to take up the most space possible, regardless of what frame is given. IE I end up with a view that looks like Diagram 2 even when no frame is specified.
// HStack{A Spacer B}
|[A B]|
I've been trying to figure out a way to tell a Spacer to prefer to be as small as possible, but to no avail. What other options do we have to achieve this layout?
Edit: To help out, here's some code as a starting point:
struct ContentView: View {
#State var largeFrame: Bool = false
var body: some View {
VStack{
Toggle("Large Frame", isOn: $largeFrame)
HStack {
Text("A")
.border(Color.red, width: 1)
Text("B")
.border(Color.red, width: 1)
}
.padding()
.frame(maxWidth: largeFrame ? .infinity : nil)
.border(Color.blue, width: 1)
}
}
}
I'm a little confused to what you are saying. Are you asking how to generate space between A and B without forcing the HStack to be window width? If so, if you place a frame on the HStack, then the spacer shoulder only separate the contents to as far as the user desires?
struct ContentView: View {
var body: some View {
HStack() {
Text("A")
Spacer()
Text("B")
}
.frame(width: 100)
}
}
EDIT:
Does the following code work? The HStack(spacing: 0) ensures that the contents the HStack have no spacing between the items and so the "smallest" possible.
struct ContentView: View {
#State private var customSpacing = true
#State private var customFrame = CGFloat(100)
var body: some View {
VStack {
Button {
customSpacing.toggle()
} label: {
Text("Custom or Not")
}
if !customSpacing {
HStack(spacing: 0) {
Text("A")
Text("B")
}
} else {
HStack(spacing: 0) {
Text("A")
Spacer()
Text("B")
}
.frame(width: customFrame)
}
}
}
}
If MyView is your component and you have control over its content, then a possible approach is to "override" .frame modifiers (all of them, below is one for demo) and compare explicitly outer width provided by frame and inner width of content subviews.
Tested with Xcode 13.4 / iOS 15.5
Main parts:
struct MyView: View { // << your component
var outerWidth: CGFloat? // << injected width !!
#State private var myWidth = CGFloat.zero // << own calculated !!
// ...
"overridden" frame modifier to store externally provided parameter
#inlinable public func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center) -> some View {
var newview = self
newview.outerWidth = maxWidth // << inject frame width !!
return VStack { newview } // << container to avoid cycling !!
.frame(minWidth: minWidth, idealWidth: idealWidth, maxWidth: maxWidth, minHeight: minHeight, idealHeight: idealHeight, maxHeight: maxHeight, alignment: alignment)
}
and conditionally activated space depending on width diffs
SubViewA()
.background(GeometryReader {
Color.clear.preference(key: ViewSideLengthKey.self,
value: $0.frame(in: .local).size.width)
})
if let width = outerWidth, width > myWidth { // << here !!
Spacer()
}
SubViewB()
.background(GeometryReader {
Color.clear.preference(key: ViewSideLengthKey.self,
value: $0.frame(in: .local).size.width)
})
Test module is here

SwiftUI re-orderable non full screen list

The intended goal is a View that expands in height to show all its elements (no scrolling), that can allow its elements to be drag and dropped to re ordered them. I believe a SwiftUI list will support this drag and drop behaviour however when placed in a VStack with other elements, it limits it's height to be the size of one element. I presume these List views aren't intended to be used within another view?
List {
ForEach(viewModel.sections, id: \.self) { section in
HStack {
Text(section.localisedString)
Spacer()
Image(systemName: "line.horizontal.3")
.foregroundColor(ColourPalette.bodyText)
.smallIcon()
}
.background(ColourPalette.background)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.background(ColourPalette.background)
}
.listStyle(PlainListStyle())
.background(ColourPalette.background)
I would also like to hide the separators however this appears to be an ongoing struggling with SwiftUI but even the SideBarListStyle() doesn't work for me.
Does it make more sense to use a VStack() and if so how easily can the reorder on drag drop and classic list side bar items be implemented?
You have tow options :
1) Using List
For placing the VStack with your List , use Section with header / footer, like that it will scroll with the list
List {
Section(header: Text("Your header here")) {
ForEach(viewModel.sections, id: \.self) { section in
HStack {
Text(section.localisedString)
Spacer()
Image(systemName: "line.horizontal.3")
.foregroundColor(ColourPalette.bodyText)
.smallIcon()
}
.background(ColourPalette.background)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.background(ColourPalette.background)
}
}
And for removidng the separator (IOS 13 support):
Adding UITableView.appearance().separatorColor = .clear anywhere in your code before the List appears should work. While this solution removes the separators, note that all List instances will be bound to this style as there’s no official way currently to only remove separators of specific instances. You may be able to run this code in onAppear and undo it in onDisappear to keep styles different.
2) Use VStack / LazyVStack

SwiftUI List view have a different shade of color after I upgraded to Xcode 12

After I upgraded to Xcode 12, my List view have a different shade of white or black (depending on Light mode or Dark mode) compared to its background. But my other List view in the same app is alright. Why?
Below are the screenshots and accompanying code. Please advise, thank you.
List
{
VStack
{
Picker("Numbers", selection: self.$selectorIndex)
{
ForEach(0..<self.numbers.count)
{
index in Text(self.numbers[index]).tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
if numbers[selectorIndex]=="Alphabetically"
{
ForEach(appointments_title)
{
order in
HStack
{
Text("\(order.title)").font(.headline)
Spacer()
Text("\(order.date, formatter: ContentView.self.taskDateFormat)").font(.headline)
}.padding(.bottom).contentShape(Rectangle())
.onTapGesture { order.isExpanded.toggle() }//.animation(.linear(duration: 0.3))
if order.isExpanded { Text("\(order.descript)").frame(maxWidth: .infinity, alignment: .topLeading).padding(.bottom) }
else { EmptyView() }
}
}
}
This is default behavior and will have something to do with the formatting of the list. I would compare both sets of code and check:
They may have different parent view structures (ie. placed differently within NavigationViews),
You may be using Sections or Groups in your code,
You can also add one of the ListStyle modifiers on the List to override their default style:
.listStyle(PlainListStyle())
.listStyle(InsetListStyle())
.listStyle(GroupedListStyle())
.listStyle(SidebarListStyle())
.listStyle(InsetGroupedListStyle())
.listStyle(DefaultListStyle())

How to create slot-machine metaphor in SwiftUI Picker?

Is it possible to create a slot machine with swiftUI to show two sets of values?
In UIKit, UIPickerView provides the option to have multiple components in your picker view. SwiftUI's Picker does not. However, you can use more than one Picker in an HStack instead. The perspective may look slightly different than a UIPickerView with multiple components in some instances, but to me it looks perfectly acceptable.
Here's an example of a slot machine with 4 pickers side by side and a button that "spins" the slot-machine when tapped (note that I disabled user interaction on the pickers so they can only be spun using the button):
enum Suit: String {
case heart, club, spade, diamond
var displayImage: Image {
return Image(systemName: "suit.\(self.rawValue).fill")
}
}
struct ContentView: View {
#State private var suits: [Suit] = [.heart, .club, .spade, .diamond]
#State private var selectedSuits: [Suit] = [.heart, .heart, .heart, .heart]
var body: some View {
VStack {
HStack(spacing: 0) {
ForEach(0..<self.selectedSuits.count, id: \.self) { index in
Picker("Suits", selection: self.$selectedSuits[index]) {
ForEach(self.suits, id: \.self) { suit in
suit.displayImage
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.clipped()
.disabled(true)
}
}
Button(action: self.spin) {
Text("Spin")
}
}
}
private func spin() {
self.selectedSuits = self.selectedSuits.map { _ in
self.suits.randomElement()!
}
}
}
This is just an example, and could no doubt be improved, but it's a decent starting point.
Keep in mind that this code does throw a warning in Xcode Beta 5 -
'subscript(_:)' is deprecated: See Release Notes for migration path.
I haven't had a chance to look into this, but the example still works and should help you with what you're trying to achieve.