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)
}
}
}
}
}
Related
I'm trying to implement a view that displays error message for my whole app.
I want this view to always be above every other view, but I also use sheets in my app and in that case the error message is hidden behind the sheet, since the sheet is displayed above every other view.
Here is a View to reproduce my situation:
struct AppView: View {
#State var isPresentingSheet = false
var body: some View {
ZStack {
VStack {
Button("Toggle sheet") {
isPresentingSheet.toggle()
}
}
.sheet(isPresented: $isPresentingSheet) {
Text("Im above everything else")
}
VStack {
HStack {
Image(systemName: "xclose")
Text("I want to be even above the sheet")
}
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(Padding.l)
.background(Color.red.opacity(0.2))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.red),
alignment: .bottom
)
Spacer()
}
}
}
}
I want to know if it's possible to display a view above a sheet, but to me it looks like the sheet is in a completely different window?
But maybe it's possible to create a custom sheet that moves in from the top and is displayed above other native sheets?
If anybody is interested, I have created a custom bottom sheet with simple controls and snap functionality.
The bottom sheet has a zIndex of 1 so you can easily place views above it with a greater zIndex.
You can find it here: AlternativeSheet.
Here is the code to achieve the view of the image above.
import AlternativeSheet
...
ZStack {
VStack {
Button("Toggle sheet") {
isPresentingSheet.toggle()
}
}
.alternativeSheet(isPresented: $isPresentingSheet, snaps: [0.95]) {
Text("Im above everything else")
}
.isDraggable()
.dampenDrag()
VStack {
HStack {
Image(systemName: "xclose")
Text("I want to be even above the sheet")
}
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(Padding.l)
.background(Color.red.opacity(0.3))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.red),
alignment: .bottom
)
Spacer()
}
}
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!
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!
Is it possible to make paged TabView that wraps its content? I don't know the height of the content (it is an Image resized to fit the width of the screen) so I can't use frame modifier.
My code looks like this:
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(width: 20, height: 20)
}
The problem is that the TabView is as big as the screen and PageIndicator is placed in the top right corner of the screen instead of top right corner of the image. Tanks for any suggestions.
EDIT:
I've added code that is reproducible. PageIndicator was replaced by red rectangle.
struct Test: View {
struct Entry: Identifiable {
let image: Image
let description: String
var id: String { description }
}
let data = [
Entry(image: Image(systemName: "scribble"), description: "image 1"),
Entry(image: Image(systemName: "trash"), description: "image 2")
]
var body: some View {
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(width: 20, height: 20)
}
}
}
Your PageIndicator is not placed on the image, because you didn't place it there. You are placing it in a layer on top of a VStack that happens to contain text and an image that can be shorter than the screen. If you want the PageIndicator on the image, then you need to do that specifically. You didn't provide a Minimal, Reproducible Example, but does something like this work:
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.overlay(
PageIndicator()
)
.frame(maxWidth: .infinity)
.overlay(
PageIndicator()
)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
The .overlay() needs to go before the .frame() so it stays on the image, instead of overlaying the .frame().
Edit:
Based off of the MRE and the comment, here is alternate solution that aligns the PageIndicator to the top of the image, but does not scroll with the image. Please note that this is not a perfect MRE as the image heights are different, but this solution actually accounts for that as well. Lastly, I added a yellow background on the image to show that things are aligned properly.
struct Test: View {
struct Entry: Identifiable {
let image: Image
let description: String
var id: String { description }
}
let data = [
Entry(image: Image(systemName: "scribble"), description: "image 1"),
Entry(image: Image(systemName: "trash"), description: "image 2")
]
#State private var imageTop: CGFloat = 50
var body: some View {
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.background(Color.yellow)
.background(
GeometryReader { imageProxy in
Color.clear.preference(
key: ImageTopInGlobal.self,
// This returns the top of the image relative to the TabView()
value: imageProxy.frame(in: .named("TabView")).minY)
}
)
Text(entry.description)
}
}
}
// This gives a reference to another container for comparison
.coordinateSpace(name: "TabView")
.tabViewStyle(.page(indexDisplayMode: .never))
VStack {
Spacer()
.frame(height: imageTop)
Color.red
.frame(width: 20, height: 20)
}
}
// You either have to ignore the safe area or account for it with regard to the TabView(). This was simpler.
.edgesIgnoringSafeArea(.top)
.onPreferenceChange(ImageTopInGlobal.self) {
imageTop = $0
}
}
}
private extension Test {
struct ImageTopInGlobal: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = nextValue()
}
}
}
Final edit:
In response to the last comment, my answer is a qualified no. I don't think there is any way to make a TabView hug its contents. (I take the original question using the term wrap to mean hug, as the TabView always "wraps" its contents.) If you try to use a preference key, the TabView collapses. There would have to be a minHeight or height set to prevent this which defeats the purpose of the hugging attempt.
I hope to make a list just like to-do list, the cell has a checkbox and Text. And I want when list cell selected, the view could be translate to detailView. Meanwhile I also could select the checkbox, but don't trigger the view translate to the detailView.
I removed the List{}, the code run ok, but the List property and method can't use any more. So I plan to code a customer list view. But I don't think it will be the good method. So if I still can't find a good way to make it, I may implement in this way at last.
NavigationView {
VStack {
List {
ForEach(todayDietModel.todayDietItems) { item in
NavigationLink(destination: DietItemDetailView()) {
TodayDietRow(todayDietItem: item)
.animation(.spring())
}
}
}
}
.frame(width: 352, height: 400)
.background(Color.white)
.cornerRadius(16)
}
struct TodayDietRow: View {
#State var isFinished: Bool = false
var body: some View {
HStack(spacing: 20.0) {
Button(action: {self.isFinished.toggle()}) {
if self.isFinished {
Image("icon_checked_s001")
.resizable()
.renderingMode(.original)
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
} else {
Image("icon_unchecked_s001")
.resizable()
.frame(width: 24, height: 24)
}
}
.padding(.leading, 10)
Text("DietName")
.font(.system(size:13))
.fontWeight(.medium)
.foregroundColor(Color.black)
}
.frame(width: 327, height: 48)
}
}
This works quite well with a gesture on the checkbox image. Two Buttons didn't work since every tap went to both buttons.
struct TodayTodoView2: View {
#ObservedObject var todayDietModel = TodayDietModel()
func image(for state: Bool) -> Image {
return state ? Image(systemName: "checkmark.circle") : Image(systemName: "circle")
}
var body: some View {
NavigationView {
VStack {
List {
ForEach($todayDietModel.todayDietItems) { (item: Binding<TodayDietItem>) in
HStack{
self.image(for: item.value.isFinished).onTapGesture {
item.value.isFinished.toggle()
}
NavigationLink(destination: DietItemDetailView2(item: item)) {
Text("DietNamee \(item.value.dietName)")
.font(.system(size:13))
.fontWeight(.medium)
.foregroundColor(Color.black)
}
}
}
}
.background(Color.white)
}
}
}
}