How to make VStack fill the width of screen in SwiftUI - ios

I'm trying to make VStack Sample app which visualize VStack Properties
but my VStack couldn't fill the width of screen
I tried many solutions on internet (frame modifier, HStack with Spacer, GeometryReader) but they were not work
This is my code
Parent View
struct LayoutView: View {
#State private var spacing: CGFloat = 0.0
#State private var alignmentIndex = 0
#State private var elementsAmount = 0
private let layout: StackLayout
private let alignments: [String] = [".leading", ".center", ".trailing"]
private let minValue: CGFloat = 0.0
private let maxValue: CGFloat = 100.0
init(_ layout: StackLayout) {
self.layout = layout
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Controls")) {
VStack(alignment: .leading) {
Text("Spacing: \(Int(spacing))").font(.caption)
HStack {
Text("\(Int(minValue))")
Slider(value: $spacing, in: minValue...maxValue, step: 1)
Text("\(Int(maxValue))")
}
}
Picker("alignments", selection: self.$alignmentIndex) {
ForEach(0..<alignments.count) {
Text(self.alignments[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
Stepper("Element's amount: \(elementsAmount)", value: self.$elementsAmount, in: 0...10)
}
Section(header: Text("Canvas")) {
VStackView(spacing: $spacing, alignmentIndex: $alignmentIndex, elementsCount: $elementsAmount)
}
}
.navigationBarTitle(Text(layout.rawValue), displayMode: .inline)
}
}
}
Child View
struct VStackView: View {
#Binding var spacing: CGFloat
#Binding var alignmentIndex: Int
#Binding var elementsCount: Int
private let alignments: [HorizontalAlignment] = [.leading, .center, .trailing]
var body: some View {
VStack(alignment: self.alignments[alignmentIndex], spacing: CGFloat(spacing)) {
ForEach(0..<elementsCount, id: \.self) {
Text("\($0)th View")
}
}
}
}
and this is result

I guess you can update code like this
struct VStackView: View {
#Binding var spacing: CGFloat
#Binding var alignmentIndex: Int
#Binding var elementsCount: Int
private let alignments: [HorizontalAlignment] = [.leading, .center, .trailing]
var body: some View {
VStack(alignment: self.alignments[alignmentIndex], spacing: CGFloat(spacing)) {
ForEach(0..<elementsCount, id: \.self) {
Text("\($0)th View").frame(maxWidth: .infinity, alignment: Alignment(horizontal: self.alignments[self.alignmentIndex], vertical: .center))
}
}
}
}

You can set maxWidth of the frame to infinity - it is a way of telling the parent view that this view wants all the width it can get:
VStack {
//...
} .frame(maxWidth: .infinity)

Related

How to fit horizontal ScrollView to content's height?

I have a horizontal scroll view with a LazyHStack. How do I size the scroll view to automatically fit the content inside?
By default, ScrollView takes up all the vertical space possible.
struct ContentView: View {
var numbers = 1...100
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(numbers, id: \.self) {
Text("\($0)")
.font(.largeTitle)
}
}
}
}
}
This code can be MUCH better
I have show it as BASE for your final solution
But it works
struct ContentView1111: View {
var numbers = 1...100
var body: some View {
VStack {
FittedScrollView(){
AnyView(
LazyHStack {
ForEach(numbers, id: \.self) {
Text("\($0)")
.font(.largeTitle)
}
}
)
}
// you can see it on screenshot - scrollView size
.background(Color.purple)
Spacer()
}
// you can see it on screenshot - other background
.background(Color.green)
}
}
struct HeightPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat = 40
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct FittedScrollView: View {
var content: () -> AnyView
#State private var contentHeight: CGFloat = 40
var body: some View {
VStack {
ScrollView(.horizontal) {
content()
.overlay(
GeometryReader { geo in
Color.clear
.preference(key: HeightPreferenceKey.self, value: geo.size.height)
})
}
.frame(height: contentHeight)
}
.onPreferenceChange(HeightPreferenceKey.self) {
contentHeight = $0
}
}
}

SwiftUI - Segment control using buttons not deselecting other buttons properly

I am trying to make a custom segment control using a button array.
When a button is tapped, the correct value is passed to the main view and can be pressed multiple times without issue. The problem is that the previous button which was selected stays selected. The code below should work to recreate the issue. Thanks for the help.
Segment view
struct WaveTypeGridView: View {
var waveTypes = ["Beach Break" ,"Reef Break", "Point Break", "Rivermouth"]
#Binding var waveTypeSelected: String
#State var typeSelected: String
let columnSpacing: CGFloat = 5
let rowSpacing: CGFloat = 10
var gridLayout: [GridItem] {
return Array(repeating: GridItem(.flexible(), spacing: rowSpacing), count: 1)
}
var body: some View {
VStack {
Text("Wave Type \(typeSelected)")
ScrollView(.horizontal, showsIndicators: false, content: {
LazyHGrid(rows: gridLayout, alignment: .center, spacing: columnSpacing, pinnedViews: [], content: {
ForEach(waveTypes, id: \.self) { type in
if type == typeSelected {
TypeItemView(selected: true, typeSelected: self.$typeSelected, name: type)
} else {
TypeItemView(selected: false, typeSelected: self.$typeSelected, name: type)
}
}
})//: GRID
.frame(height: 18)
.padding(.vertical, 8)
.padding(.horizontal, 10)
})//: SCROLL
.background(Color(.systemGray5).cornerRadius(8))
}
}
}
struct WaveTypeGridView_Previews: PreviewProvider {
static var previews: some View {
WaveTypeGridView(waveTypeSelected: .constant(surfDataTests[0].waveType), typeSelected: "Beach Break")
.previewLayout(.sizeThatFits)
}
}
Button View
struct TypeItemView: View {
#State var selected: Bool
#Binding var typeSelected: String
let name: String
func test() {
}
var body: some View {
Button(action: {
selected.toggle()
typeSelected = name
}, label: {
HStack(alignment: .center, spacing: 6, content: {
Text(name)
.font(.footnote)
.fontWeight(.medium)
.foregroundColor(selected ? Color.white : Color.black)
})//: HSTACK
.frame(height: 18)
.padding(6)
.background(selected ? Color.blue
.cornerRadius(8)
: Color.white
.cornerRadius(8))
.background (
RoundedRectangle(cornerRadius: 8)
)
})//: BUTTON
}
}
struct TypeItemView_Previews: PreviewProvider {
static var previews: some View {
TypeItemView(selected: false, typeSelected: .constant("Rivermouth"), name:"Rivermouth")
.previewLayout(.sizeThatFits)
.padding()
.background(Color.gray)
}
}
You seem to have made your state tracking more complex than it needs to be. All you need to know is the currently selected wave type. You don't need to track a separate selected state, since the button can determine this from it's own value and the currently selected value.
For the grid view you can just have a single #State property for the selected wave type (You could inject this as an #Binding if required).
Pass the selected break type and the view's target value to the TypeItemView
struct WaveTypeGridView: View {
var waveTypes = ["Beach Break" ,"Reef Break", "Point Break", "Rivermouth"]
#State var typeSelected: String = "Beach Break"
let columnSpacing: CGFloat = 5
let rowSpacing: CGFloat = 10
var gridLayout: [GridItem] {
return Array(repeating: GridItem(.flexible(), spacing: rowSpacing), count: 1)
}
var body: some View {
VStack {
Text("Wave Type \(typeSelected)")
ScrollView(.horizontal, showsIndicators: false, content: {
LazyHGrid(rows: gridLayout, alignment: .center, spacing: columnSpacing, pinnedViews: [], content: {
ForEach(waveTypes, id: \.self) { type in
TypeItemView(typeSelected: self.$typeSelected, name: type)
}
})//: GRID
.frame(height: 18)
.padding(.vertical, 8)
.padding(.horizontal, 10)
})//: SCROLL
.background(Color(.systemGray5).cornerRadius(8))
}
}
}
Then in your TypeItemView you can create a computed property for selected based on the current value and this view's target value.
struct TypeItemView: View {
#Binding var typeSelected: String
let name: String
private var selected: Bool {
return typeSelected == name
}
func test() {
}
var body: some View {
Button(action: {
typeSelected = name
}, label: {
HStack(alignment: .center, spacing: 6, content: {
Text(name)
.font(.footnote)
.fontWeight(.medium)
.foregroundColor(self.selected ? Color.white : Color.black)
})//: HSTACK
.frame(height: 18)
.padding(6)
.background(self.selected ? Color.blue
.cornerRadius(8)
: Color.white
.cornerRadius(8))
.background (
RoundedRectangle(cornerRadius: 8)
)
})//: BUTTON
}
}
Now the whole control only depends on one #State item.

SwiftUI Get Coordinates of TextField or any View

I'm struggling to find a way in SwiftUI to get the x,y coordinates of the origin of a
TextField (or any view). I can certainly provide a position or offset to move the view
but I can't seem to find a way to get a reference to the TextField to get its
coordinates.
In UIKit, I believe I would use myTextField.frame.origin.x
This is a very simple example:
struct ContentView: View {
#State private var someNumber1 = "1000"
#State private var someNumber2 = "2000"
//bunch more
#State private var enteredNumber = "Some Text at the Top"
#State var value: CGFloat = 0
var body: some View {
ScrollView {
VStack {
Spacer()
Text("\(enteredNumber)")
Spacer()
Group { //1
TextField("Placeholder", text: $someNumber1)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
//this does not work
.onTapGesture {
let aY = (textfieldreferencehere).frame.origin.y
}
TextField("Placeholder", text: $someNumber2)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
}//group 1
//bunch more
Button(action: {
self.enteredNumber = self.someNumber1
self.someNumber1 = ""
UIApplication.shared.endEditing()
}) {
Text("Submit")
}
.padding(.bottom, 50)
}//outer v
.padding(.horizontal, 16)
.padding(.top, 44)
}//Scrollview or Form
.modifier(AdaptsToSoftwareKeyboard())
}
}
Any Guidance would be appreciated. Xcode 11.4.1
Here is a demo of how specific view coordinates can be read (see helpful comments inline)
struct DemoReadViewOrigin: View {
#State private var someNumber1 = "1000"
#State private var someNumber2 = "2000"
//bunch more
#State private var enteredNumber = "Some Text at the Top"
#State var value: CGFloat = 0
var body: some View {
ScrollView {
VStack {
Spacer()
Text("\(enteredNumber)")
Spacer()
Group { //1
TextField("Placeholder", text: $someNumber1)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(GeometryReader { gp -> Color in
let rect = gp.frame(in: .named("OuterV")) // < in specific container
// let rect = gp.frame(in: .global) // < in window
// let rect = gp.frame(in: .local) // < own bounds
print("Origin: \(rect.origin)")
return Color.clear
})
//this does not work
.onTapGesture {
}
TextField("Placeholder", text: $someNumber2)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
}//group 1
//bunch more
Button(action: {
self.enteredNumber = self.someNumber1
self.someNumber1 = ""
// UIApplication.shared.endEditing()
}) {
Text("Submit")
}
.padding(.bottom, 50)
}//outer v
.coordinateSpace(name: "OuterV") // << declare coord space
.padding(.horizontal, 16)
.padding(.top, 44)
}//Scrollview or Form
// .modifier(AdaptsToSoftwareKeyboard())
}
}

SwiftUI Create a Custom Segmented Control also in a ScrollView

Below is my code to create a standard segmented control.
struct ContentView: View {
#State private var favoriteColor = 0
var colors = ["Red", "Green", "Blue"]
var body: some View {
VStack {
Picker(selection: $favoriteColor, label: Text("What is your favorite color?")) {
ForEach(0..<colors.count) { index in
Text(self.colors[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(colors[favoriteColor])")
}
}
}
My question is how could I modify it to have a customized segmented control where I can have the boarder rounded along with my own colors, as it was somewhat easy to do with UIKit? Has any one done this yet.
I prefect example is the Uber eats app, when you select a restaurant you can scroll to the particular portion of the menu by selecting an option in the customized segmented control.
Included are the elements I'm looking to have customized:
* UPDATE *
Image of the final design
Is this what you are looking for?
import SwiftUI
struct CustomSegmentedPickerView: View {
#State private var selectedIndex = 0
private var titles = ["Round Trip", "One Way", "Multi-City"]
private var colors = [Color.red, Color.green, Color.blue]
#State private var frames = Array<CGRect>(repeating: .zero, count: 3)
var body: some View {
VStack {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
}.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)).background(
GeometryReader { geo in
Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
}
)
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
Picker(selection: self.$selectedIndex, label: Text("What is your favorite color?")) {
ForEach(0..<self.titles.count) { index in
Text(self.titles[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames[index] = frame
}
}
struct CustomSegmentedPickerView_Previews: PreviewProvider {
static var previews: some View {
CustomSegmentedPickerView()
}
}
If I'm following the question aright the starting point might be something like the code below. The styling, clearly, needs a bit of attention. This has a hard-wired width for segments. To be more flexible you'd need to use a Geometry Reader to measure what was available and divide up the space.
struct ContentView: View {
#State var selection = 0
var body: some View {
let item1 = SegmentItem(title: "Some Way", color: Color.blue, selectionIndex: 0)
let item2 = SegmentItem(title: "Round Zip", color: Color.red, selectionIndex: 1)
let item3 = SegmentItem(title: "Multi-City", color: Color.green, selectionIndex: 2)
return VStack() {
Spacer()
Text("Selected Item: \(selection)")
SegmentControl(selection: $selection, items: [item1, item2, item3])
Spacer()
}
}
}
struct SegmentControl : View {
#Binding var selection : Int
var items : [SegmentItem]
var body : some View {
let width : CGFloat = 110.0
return HStack(spacing: 5) {
ForEach (items, id: \.self) { item in
SegmentButton(text: item.title, width: width, color: item.color, selectionIndex: item.selectionIndex, selection: self.$selection)
}
}.font(.body)
.padding(5)
.background(Color.gray)
.cornerRadius(10.0)
}
}
struct SegmentButton : View {
var text : String
var width : CGFloat
var color : Color
var selectionIndex = 0
#Binding var selection : Int
var body : some View {
let label = Text(text)
.padding(5)
.frame(width: width)
.background(color).opacity(selection == selectionIndex ? 1.0 : 0.5)
.cornerRadius(10.0)
.foregroundColor(Color.white)
.font(Font.body.weight(selection == selectionIndex ? .bold : .regular))
return Button(action: { self.selection = self.selectionIndex }) { label }
}
}
struct SegmentItem : Hashable {
var title : String = ""
var color : Color = Color.white
var selectionIndex = 0
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
None of the above solutions worked for me as the GeometryReader returns different values once placed in a Navigation View that throws off the positioning of the active indicator in the background. I found alternate solutions, but they only worked with fixed length menu strings. Perhaps there is a simple modification to make the above code contributions work, and if so, I would be eager to read it. If you're having the same issues I was, then this may work for you instead.
Thanks to inspiration from a Reddit user "End3r117" and this SwiftWithMajid article, https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/, I was able to craft a solution. This works either inside or outside of a NavigationView and accepts menu items of various lengths.
struct SegmentMenuPicker: View {
var titles: [String]
var color: Color
#State private var selectedIndex = 0
#State private var frames = Array<CGRect>(repeating: .zero, count: 5)
var body: some View {
VStack {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: {
print("button\(index) pressed")
self.selectedIndex = index
}) {
Text(self.titles[index])
.foregroundColor(color)
.font(.footnote)
.fontWeight(.semibold)
}
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
.modifier(FrameModifier())
.onPreferenceChange(FramePreferenceKey.self) { self.frames[index] = $0 }
}
}
.background(
Rectangle()
.fill(self.color.opacity(0.4))
.frame(
width: self.frames[self.selectedIndex].width,
height: 2,
alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX, y: self.frames[self.selectedIndex].height)
, alignment: .leading
)
}
.padding(.bottom, 15)
.animation(.easeIn(duration: 0.2))
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
}
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
struct FrameModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: FramePreferenceKey.self, value: geometry.frame(in: .global))
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}
struct NewPicker_Previews: PreviewProvider {
static var previews: some View {
VStack {
SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.blue)
NavigationView {
SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.red)
}
}
}
}

SwiftUI: Transition won't slide

I have control with an edit and a list in a Stack. I want the list to drop down from top to bottom to animate like a fancy menu. However I am having a few issues. If I just try to use the move transition nothing happens. If I use the scale transition it always scales form center never from top down. This was just trying to get the transition overridden to slide. Anything but fade.
My control looks like so
struct Search: Identifiable {
let id: UUID
let text: String
}
struct SearchBox: View {
#State var searchParam: String = ""
#State var stuff = [Search]()
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
}
var body: some View {
var binding = Binding<String>(
get: {
self.searchParam
},
set: {
self.stuff.append(
Search(id: UUID(), text: $0))
self.searchParam = $0
})
return VStack(spacing: 0.0) {
TextField("Search", text: binding )
.font(.title)
.padding()
.background(Color.white)
Color(.darkGray)
.frame(height: 1.0)
if stuff.count > 0 {
List(stuff, id: \.id) {
Text($0.text)
}
.transition(.slide)
}
}
}
struct SearchBox_Preview: PreviewProvider {
static var previews: some View{
SearchBox()
}
}
}
The content view is simple..
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .topLeading) {
Color.blue
SearchBox()
.frame(width: geometry.size.width * 0.40, alignment: .topLeading)
.frame(minHeight: 0, maxHeight: geometry.size.height * 0.40,
alignment: .topLeading)
.padding()
.clipped()
.shadow(radius: 5.0)
}
.background(Color.clear)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have to be missing, or not getting, something simple
Answered my own question. We need to force an animation by using withAnimation on a state. So I have changed my biding to be like so:
struct Search: Identifiable {
let id: UUID
let text: String
}
struct SearchBox: View {
#State var searchParam: String = ""
#State var stuff = [Search]()
#State var showList = false
init() {
// To remove only extra separators below the list:
UITableView.appearance().tableFooterView = UIView()
// To remove all separators including the actual ones:
UITableView.appearance().separatorStyle = .none
}
var body: some View {
var binding = Binding<String>(
get: {
self.searchParam
},
set: {
self.stuff.append(
Search(id: UUID(), text: $0)
)
self.searchParam = $0
// change visibility state with in animation block.
withAnimation { self.showList = stuff.count > 0 }
})
return VStack(spacing: 0.0) {
TextField("Search", text: binding )
.font(.title)
.padding()
.background(Color.white)
Color(.darkGray)
.frame(height: 1.0)
if showList {
List(stuff, id: \.id) {
Text($0.text)
}
.transition(.slide)
}
}
}
struct SearchBox_Preview: PreviewProvider {
static var previews: some View{
SearchBox()
}
}
}

Resources