I am trying to display images stored in an array which I am providing as a collection to ForEach and giving it a HStack view to display the result. But for life of me, can't fathom why HStack is returning a "VStack" sort of view?
Code:
import SwiftUI
struct ContentView: View {
var body: some View {
Home()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Home: View {
// 40 = padding horizontal
// 60 = 2 card to right side...
var width = UIScreen.main.bounds.width - (40 + 60)
var height = UIScreen.main.bounds.height/2
var books = [
Book(id: 0, image: "p1", offset: 0),
Book(id: 1, image: "p0", offset: 0),
Book(id: 2, image: "p3", offset: 0),
Book(id: 3, image: "p2", offset: 0),
Book(id: 4, image: "p5", offset: 0),
Book(id: 5, image: "p4", offset: 0),
]
var body: some View{
ForEach(books.reversed()) { book in
HStack{
Image(book.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: width, height:getheight(index: book.id))
.cornerRadius(25)
.shadow(color: Color.black.opacity(0.5), radius: 5, x: 5)
}
}
}
func getheight(index: Int)->CGFloat{
return height - (index < 3 ? CGFloat(index) * 40 : 80)
}
}
struct Book : Identifiable {
var id: Int
var image : String
var offset : CGFloat
}
I have stripped the code to barebones to highlight the issue and have attached my output screenshot for clarity. Please help.
.
The problem is the HStack being inside the ForEach. You are not aligning every view in an HStack, but just each separate view in its own HStack. It seems that by default SwiftUI prefers a vertical layout.
Consider this incorrect code:
struct ContentView: View {
var body: some View {
ForEach(1 ..< 10) { row in
HStack {
Text(String(row))
}
}
}
}
And this correct code:
struct ContentView: View {
var body: some View {
HStack {
ForEach(1 ..< 10) { row in
Text(String(row))
}
}
}
}
These are the results. Left is HStack inside the ForEach, and right is HStack outside the ForEach:
Related
I'm trying to use a LazyVGrid in SwiftUI where you can touch and drag your finger to select multiple adjacent cells in a specific order. This is not a drag and drop and I don't want to move the cells (maybe drag isn't the right term here, but couldn't think of another term to describe it). Also, you would be able to reverse the selection (ie: each cell can only be selected once and reversing direction would un-select the cell). How can I accomplish this? Thanks!
For example:
struct ContentView: View {
#EnvironmentObject private var cellsArray: CellsArray
var body: some View {
VStack {
LazyVGrid(columns: gridItems, spacing: spacing) {
ForEach(0..<(rows * columns), id: \.self){index in
VStack(spacing: 0) {
CellsView(index: index)
}
}
}
}
}
}
struct CellsView: View {
#State var index: Int
#EnvironmentObject var cellsArray: CellsArray
var body: some View {
ZStack {
Text("\(self.cellsArray[index].cellValue)") //cellValue is a string
.foregroundColor(Color.yellow)
.frame(width: getWidth(), height: getWidth())
.background(Color.gray)
}
//.onTapGesture ???
}
func getWidth()->CGFloat{
let width = UIScreen.main.bounds.width - 10
return width / CGFloat(columns)
}
}
Something like this might help, the only issue here is the coordinate space. Overlay is not drawing the rectangle in the correct coordinate space.
struct ImagesView: View {
var columnSize: CGFloat
#Binding var projectImages: [ProjectImage]
#State var selectedImages: [ImageSelection] = []
#State var dragWidth: CGFloat = 1.0
#State var dragHeight: CGFloat = 1.0
#State var dragStart: CGPoint = CGPoint(x:0, y:0)
let columns = [
GridItem(.adaptive(minimum: 200), spacing: 0)
]
var body: some View {
GeometryReader() { geometry in
ZStack{
ScrollView{
LazyVGrid(columns: [
GridItem(.adaptive(minimum: columnSize), spacing: 2)
], spacing: 2){
ForEach(projectImages, id: \.imageUUID){ image in
Image(nsImage: NSImage(data: image.imageData)!)
.resizable()
.scaledToFit()
.border(selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) ? Color.blue : .primary, width: selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) ? 5.0 : 1.0)
.gesture(TapGesture(count: 2).onEnded{
print("Double tap finished")
})
.gesture(TapGesture(count: 1).onEnded{
if selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) {
print("Image is already selected")
if let index = selectedImages.firstIndex(where: {imageSelection in imageSelection.uuid == image.imageUUID}){
selectedImages.remove(at: index)
}
} else {
selectedImages.append(ImageSelection(imageUUID: image.imageUUID))
print("Image has been selected")
}
})
}
}
.frame(minHeight: 50)
.padding(.horizontal, 1)
}
.simultaneousGesture(
DragGesture(minimumDistance: 2)
.onChanged({ value in
print("Drag Start: \(value.startLocation.x)")
self.dragStart = value.startLocation
self.dragWidth = value.translation.width
self.dragHeight = value.translation.height
})
)
.overlay{
Rectangle()
.frame(width: dragWidth, height: dragHeight)
.offset(x:dragStart.x, y:dragStart.y)
}
}
}
}
}
How to get the position of green rectangle and set it to POSX & POSY? I'm trying to reposition it when application loads, and when I click a button the text box should move to the position of the rectangle.
import SwiftUI
class EnvironmentVariables: ObservableObject {
#Published var POSX: CGFloat = 0.0
#Published var POSY: CGFloat = 0.0
}
struct ContentView: View {
var body: some View {
ZStack {
Rectangle ()
.frame(width: 5, height: 5)
.zIndex(2)
.foregroundColor(.green)
} .frame(width: 200, height: 200, alignment: .center)
}
}
struct docker: View {
#EnvironmentObject var environmentVariables: EnvironmentVariables
//#StateObject var environmentVariables = EnvironmentVariables()
var body: some View {
VStack {
Text("The text")
.background(.green)
.position(x: environmentVariables.POSX, y: environmentVariables.POSX)
}
.environmentObject(environmentVariables)
}
}
Hi I have a Stack of Hstacks that consist of a Text and a slider. The slider width extends to the edge of the text in front of it but I want them all to have the same width and appear in a straight column.
Like this below.
This is how I am forming the stacks.
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray)
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
this is the view where I am forming the sliders. Can someone pls help
struct MyNodeView : View {
#Binding var myNode : Sliders
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.percent))%").font(.footnote)
Slider(value: $myNode.percent, in: 0 ... 100)
}
}
}
To make sure all the text is the same width, the simplest way is .frame(width:).
struct Sliders {
var name = "Name"
var percent = CGFloat(100)
}
struct ContentView: View {
#State var defensiveLayers = [
Sliders(name: "Long name", percent: 80),
Sliders(name: "Name", percent: 70),
Sliders(name: "Ok name", percent: 65),
Sliders(name: "Hi", percent: 15),
Sliders(name: "Hello", percent: 45),
]
var body: some View {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray)
.frame(width: 100) /// add frame here!
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
}
}
struct MyNodeView : View {
#Binding var myNode: Sliders
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.percent))%").font(.footnote)
Slider(value: $myNode.percent, in: 0 ... 100)
}
}
}
Result:
You could calculate the required width for the longest Text and apply that particular width to all the leading Text views in all the HStacks unifying them in the width. I've created a helper method for finding the required width for any SwiftUI View.
Helper:
extension View {
func viewSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { }
}
Usage:
#State private var textFrameWidth: CGFloat = 0
var body: some View {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
HStack {
Text(defensiveLayers[i].name)
.font(.custom("Gill Sans", size: 12))
.padding(.trailing)
.foregroundColor(.gray)
.viewSize { size in
textFrameWidth = textFrameWidth < size.width ? size.width : textFrameWidth
}
.frame(width: textFrameWidth)
MyNodeView(myNode: $defensiveLayers[i])
}
}
}
}
i am trying to add swipe inside list cell , swipe to show more options such as Delete, archive etc .
The swipe is working just fine , but the List ( vertical scroll ) is no longer scrolling up down .
Cell Bite :
import SwiftUI
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
//Swipe to custom options ,by "Jack" this option not yet available in SwiftUI
let drag = DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged {
if (self.offset.width > 0 ){ return }
self.offset.width = $0.translation.width
}.onEnded {
if $0.translation.width < -100 {
self.offset = .init(width: -100, height: 0)
} else {
self.offset = .zero
}
}
ZStack{
Rectangle().foregroundColor(.blue).offset(x: offset.width, y: offset.height)
.gesture(drag)
.animation(.easeIn, value: offset)
Text("test").foregroundColor(.white)
}.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 100,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
struct cl_task_Previews: PreviewProvider {
static var previews: some View {
cl_task().previewLayout(.sizeThatFits)
}
}
List main view :
struct Item {
let uuid = UUID()
let value: String
}
struct w_tasks: View {
#State private var items = [Item]()
var body: some View {
ZStack {
List(self.items, id: \.uuid) {item in
cl_task()
}
.simultaneousGesture(DragGesture().onChanged({ value in
//Scrolled
}))
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.items.append(Item(value: "Item"))
}, label: {
Text("+")
.font(.system(size: 50))
.frame(width: 77, height: 70)
.foregroundColor(Color.white)
.padding(.bottom, 7)
})
.background(Color(hex : "#216D94"))
.cornerRadius(38.5)
.padding()
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
}
}
}
}
}
struct w_tasks_Previews: PreviewProvider {
static var previews: some View {
w_tasks()
}
}
I've posted my question after spending hours solving this issue as i am new to SwiftUI , any advice how to solve it ?
The solution is to give different distance for swipe, like below
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
// give 25 distance makes vertical scroll enabled !!
let drag = DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onChanged {
Tested with Xcode 12.4 / iOS 14.4
My SwiftUI app has a segmented Picker and I want to be able to disable one or more options depending on availability of options retrieved from a network call. The View code looks something like:
#State private var profileMetricSelection: Int = 0
private var profileMetrics: [RVStreamMetric] = [.speed, .heartRate, .cadence, .power, .altitude]
#State private var metricDisabled = [true, true, true, true, true]
var body: some View {
VStack(alignment: .leading, spacing: 2.0) {
...(some views)...
Picker(selection: $profileMetricSelection, label: Text("")) {
ForEach(0 ..< profileMetrics.count) { index in
Text(self.profileMetrics[index].shortName).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
...(some more views)...
}
}
What I want to be able to do is modify the metricDisabled array based on network data so the view redraws enabling the relevant segments. In UIKit this can be done by calls to setEnabled(_:forSegmentAt:) on the UISegmentedControl but I can't find a way of doing this with the SwiftUI Picker
I know I can resort to wrapping a UISegmentedControl in a UIViewRepresentable but before that I just wanted to check I'm not missing something...
you can use this simple trick
import SwiftUI
struct ContentView: View {
#State var selection = 0
let data = [1, 2, 3, 4, 5]
let disabled = [2, 3] // at index 2, 3
var body: some View {
let binding = Binding<Int>(get: {
self.selection
}) { (i) in
if self.disabled.contains(i) {} else {
self.selection = i
}
}
return VStack {
Picker(selection: binding, label: Text("label")) {
ForEach(0 ..< data.count) { (i) in
Text("\(self.data[i])")
}
}.pickerStyle(SegmentedPickerStyle())
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Maybe something like
ForEach(0 ..< data.count) { (i) in
if !self.disabled.contains(i) {
Text("\(self.data[i])")
} else {
Spacer()
}
}
could help to visualize it better
NOTES (based on the discussion)
From user perspective, the Picker is one control, which could be in disabled / enabled state.
The option selected from Picker is not control, it is some value. If you make a list of controls presented to the user, some of them could be disabled, just to inform the user, that the action associated with it is not currently available (like menu, some buttons collection etc.)
I suggest you to show in Picker only values which could be selected. This collection of values could be updated any time.
UPDATE
Do you like something like this?
No problem at all ... (copy - paste - try - modify ...)
import SwiftUI
struct Data: Identifiable {
let id: Int
let value: Int
var disabled: Bool
}
struct ContentView: View {
#State var selection = -1
#State var data = [Data(id: 0, value: 10, disabled: true), Data(id: 1, value: 20, disabled: true), Data(id: 2, value: 3, disabled: true), Data(id: 3, value: 4, disabled: true), Data(id: 4, value: 5, disabled: true)]
var filteredData: [Data] {
data.filter({ (item) -> Bool in
item.disabled == false
})
}
var body: some View {
VStack {
VStack(alignment: .leading, spacing: 0) {
Text("Select from avaialable")
.padding(.horizontal)
.padding(.top)
HStack {
GeometryReader { proxy in
Picker(selection: self.$selection, label: Text("label")) {
ForEach(self.filteredData) { (item) in
Text("\(item.value.description)").tag(item.id)
}
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: CGFloat(self.filteredData.count) * proxy.size.width / CGFloat(self.data.count), alignment: .topLeading)
Spacer()
}.frame(height: 40)
}.padding()
}.background(Color.yellow.opacity(0.2)).cornerRadius(20)
Button(action: {
(0 ..< self.data.count).forEach { (i) in
self.data[i].disabled = false
}
}) {
Text("Enable all")
}
Button(action: {
self.data[self.selection].disabled = true
self.selection = -1
}) {
Text("Disable selected")
}.disabled(selection < 0)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}