SwiftUI: List containing children composed of List - swiftui

I have a view that creates a List, and using a ForEach, iterates over a list of children, and each of those children is rendered inside a view. The problem is that the child view is also composed of a List, so I end up having the following image:
(I have also uploaded a short video recorded in my simulator here; if there's a better way and service to which I can upload a video, please let me know).
Basically, the child list occupies as much as a row in the parent list, although it can be scrolled, I would like the child view to occupy more space in order to look normal.
Here's the code for my parent view:
var body: some View {
GeometryReader { proxy in
List {
let subworkoutsArray: [Subworkout] = workout.containsSubworkouts?.toArray() ?? []
ForEach(subworkoutsArray, id: \.self) { (subworkout: Subworkout) in
Section(header: SubworkoutHeaderView(subworkout: subworkout)) {
SubworkoutView(subworkout: subworkout, shouldShowFooterView: false, proxy: proxy)
}
}
HStack {
FooterTimerView(workout: self.$workout)
}
.listRowInsets(EdgeInsets())
.frame(maxWidth: .infinity, minHeight: 60)
}
.listStyle(.grouped)
.navigationTitle(workout.workoutName)
.navigationBarTitleDisplayMode(.inline)
}
}
and this is the code for the SubworkoutView:
var body: some View {
List {
ForEach(subworkout.containsExercises?.toArray() ?? [], id: \.self) { (exercise: Exercise) in
showExerciseInfo(exercise)
}
if shouldShowFooterView {
HStack {
FooterTimerView(subworkout: self.$subworkout)
}
.listRowInsets(EdgeInsets())
.frame(maxWidth: .infinity, minHeight: 60)
}
}
.listStyle(.grouped)
.if(proxy != nil, content: { content in
content.frame(width: proxy!.size.width, height: proxy!.size.height, alignment: .top)
})
}
I have tried several options, like placing the GeometryReader in the parent view right before the child view is called, but it makes it even worse.
Thanks in advance!

Related

Some NavigationLink are inaccessible

My SwiftUI TVOS app has two sets of NavigationLink. When both sets are present (not commented out), only one set is accessible to tap on. If I comment out one or the other set, the remaining NavigationLink is accessible to tap on and functions properly.
How can both sets of NavigationLink be accessible (can be interacted with)?
I've tried encapsulating my view in NavigationView and NavigationStack, neither behaved differently.
The view, as shown below, only the NavigationLinks in the ScrollView are accessible to interact with. The "Edit" NavigationLink cannot be selected to tap on. If I comment out the ScrollView NavigationLinks, then the "Edit" NavigationLink becomes accessible and functions correctly.
I've also tried replacing LazyVGrid with VStack to no effect.
import SwiftUI
struct TestSources: Hashable {
let id = UUID()
let name: String
}
struct SourcesView: View {
private var Sources = [TestSources(name: "Computer 1"), TestSources(name: "Computer 2")]
var columns: [GridItem] {
Array(repeating: .init(.adaptive(minimum: 200)), count: 2)
}
var body: some View {
NavigationStack {
VStack(alignment: .center) {
// Header
HStack(alignment: .center){
Label("Sources", systemImage: "externaldrive.connected.to.line.below")
.font(.headline)
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
.padding(.all)
NavigationLink(destination: TestEditView()) {
Text("Edit")
}
}
Divider()
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(Sources.indices, id: \.self) { index in
NavigationLink(Sources[index].name ,value: Sources[index])
}.navigationDestination(for: TestSources.self) { source in
TestShareView(source: source)
}
.accentColor(Color.black)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal)
}
}.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
}
struct TestEditView: View {
var body: some View {
Text("Edit")
}
}
struct TestShareView: View {
let source : TestSources
var body: some View {
Text(source.name)
}
}
I don't see any problem with the navigation links in this code.
I pasted the code into a new project and tweaked it a little to make it compile. As you can see, it just works.
My guess is that it might fail because something outside of this code. Maybe, it is within another NavigationStack or some structure that could increse it's navigation complexity?
Or as Yrb suggests, this force unwrapping could be failing because of null values?

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

How to change the background based on focused item in a ForEach

I'm using a ForEach within a horizontal ScrollView to display a list of movies from my movies array. I want to make an interaction like Netflix where the background updates with the an image, movie description, etc. based on the item currently in focus.
This is the best solution I've managed to come up with so far but its very far off.
My MainMenuView:
struct MainMenuView: View {
#ObservedObject var vm = MainMenuViewModel()
var body: some View {
VStack {
Text("Movies")
.font(.title3.weight(.semibold))
.frame(maxWidth: .infinity, alignment: .leading)
ScrollView(.horizontal) {
LazyHStack {
ForEach(vm.selections) { selection in
Button {
print("Hello, World!")
} label: {
ZStack {
SelectionPreview(selection: selection)
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .bottom)
Image(selection.thumbnail)
.resizable()
.scaledToFill()
.frame(width: 500, height: 281)
}
}
.cornerRadius(32)
.buttonStyle(.plain)
}
}
}
}
}
}
My SelectionPreview:
struct SelectionPreview: View {
#State var selection: Movie
#Environment(\.isFocused) var isFocused: Bool
var body: some View {
Image(isFocused ? selection.backdrop : "")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
The above code does manage to update a background based on the current item in focus but all the other items in the list get pushed to the side since the View is now the size of the background image.
I also tried to set a background on the entire VStack instead but this didn't work. I think because the Vstack on its own is not focusable:
Something like this:
struct MainMenuView: View {
var body: some View {
VStack {
ScrollView(.horizontal) {
[...]
}
}
.background(
SelectionPreview()
)
}
}
My goal is to achieve something similar to this:
Any help would be appreciated!

SwiftUI Add view below a list

I have a list with some items.
Below the list I'd like to have to button to load more items.
(As loading all items requires some user actions like entering a TAN, this should not be done automatically when the user scrolls to the end of the list, but only if he likes to.)
What I'd like to have is a view like this:
However, if I place the List and the Button in a VStack, the Button get always displayed at the bottom of the screen, not only when I scroll to the end of the List:
struct ContentView: View {
private let items = Range(0...15).map { "Item " + String($0) }
var body: some View {
VStack {
List(items, id: \.self) { item in
Text(item)
}
HStack {
Spacer()
Button("Load more") { print("Load more items") }
Spacer()
}
}
}
}
If I add the Button to the List, the Button obviously gets displayed as a List item with a white background and without any space to the list:
struct ContentView: View {
private let items = Range(0...15).map { "Item " + String($0) }
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
HStack {
Spacer()
Button("Load more") { print("Load more items") }
Spacer()
}
}.listStyle(GroupedListStyle())
}
}
Is there any way to add a view that becomes visible when the user scrolls to the end of the List but that is not part of the List? (Or at least looks like being below the List and not part of it?)
You should use second variant, but a bit tuned, like below (colors/spaces modify per your needs
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
HStack {
Button("Load more") { print("Load more items") }
}
.listRowInsets(EdgeInsets())
.frame(maxWidth: .infinity, minHeight: 60)
.background(Color(UIColor.systemGroupedBackground))
}.listStyle(GroupedListStyle())
}

List view - any way to scroll horizontally?

I have a list that loads from a parsed CSV file built using SwiftUI and I can't seem to find a way to scroll the list horizontally.
List {
// Read each row of the array and return it as arrayRow
ForEach(arrayToUpload, id: \.self) { arrayRow in
HStack {
// Read each column of the array (Requires the count of the number of columns from the parsed CSV file - itemsInArray)
ForEach(0..<self.itemsInArray) { itemNumber in
Text(arrayRow[itemNumber])
.fixedSize()
.frame(width: 100, alignment: .leading)
}
}
}
}
.frame(minWidth: 1125, maxWidth: 1125, minHeight: 300, maxHeight: 300)
.border(Color.black)
The list renders how I would like but I'm just stuck on this one point.
Preview Image Of Layout
Swift 5;
iOS 13.4
You should use an ScrollView as Vyacheslav Pukhanov suggested but in your case the scrollView size does not get updated after the async call data arrive. So you have 2 options:
Provide a default value or an alternative view.
Provide a fixed size to the HStack inside of the ForeEach. (I used this one)
I faced the same problem laying out an horizontal grid of two columns. Here's my solution
import SwiftUI
struct ReviewGrid: View {
#ObservedObject private var reviewListViewModel: ReviewListViewModel
init(movieId: Int) {
reviewListViewModel = ReviewListViewModel(movieId: movieId)
//ReviewListViewModel will request all reviews for the given movie id
}
var body: some View {
let chunkedReviews = reviewListViewModel.reviews.chunked(into: 2)
// After the API call arrive chunkedReviews will get somethig like this => [[review1, review2],[review3, review4],[review5, review6],[review7, review8],[review9]]
return ScrollView (.horizontal) {
HStack {
ForEach(0..<chunkedReviews.count, id: \.self) { index in
VStack {
ForEach(chunkedReviews[index], id: \.id) { review in
Text("*\(review.body)*").padding().font(.title)
}
}
}
}
.frame(height: 200, alignment: .center)
.background(Color.red)
}
}
}
This is a dummy example don't expect a fancy view ;)
I hope it helps you.
You should use a horizontal ScrollView instead of the List for this purpose.
ScrollView(.horizontal) {
VStack {
ForEach(arrayToUpload, id: \.self) { arrayRow in
HStack {
ForEach(0..<self.itemsInArray) { itemNumber in
Text(arrayRow[itemNumber])
.fixedSize()
.frame(width: 100, alignment: .leading)
}
}
}
}
}