SwiftUI - Draggable Slides with List - ios

I'm a bit stumped building a draggable slide containing a list. Inspired by this post, a MRE of what I have is below.
import SwiftUI
struct ContentView: View {
static let kMinHeight: CGFloat = 100.0
#State var currentHeight: CGFloat = kMinHeight // << any initial
var body: some View {
GeometryReader { g in // << for top container height limit
ZStack(alignment: .bottom) {
Rectangle().fill(Color.yellow) // << just for demo
self.card()
.gesture(DragGesture()
.onChanged { value in
// as card is at bottom the offset is reversed
let newHeight = self.currentHeight - (value.location.y - value.startLocation.y)
if newHeight > Self.kMinHeight && newHeight < g.size.height {
self.currentHeight = newHeight
}
})
}
}
}
func card() -> some View {
ZStack(alignment: .top){
RoundedRectangle(cornerRadius: 16.0)
.frame(height:currentHeight)
VStack{
RoundedRectangle(cornerRadius: Constants.radius)
.fill(Color.white)
.frame(
width: Constants.indicatorWidth,
height: Constants.indicatorHeight
)
Text("Card")
.font(.title)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.top)
Text("LINE 2")
.foregroundColor(Color.white)
// Uncomment the following lines, and the slides becomes unusable
// List {
// Text("The Fellowship of the Ring")
// Text("The Two Towers")
// Text("The Return of the King")
//}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The slides built using this example works, but note that I've commented out a few lines containing a list. Once these lines are uncommented, the slider becomes unusable.
Two obvious questions: (1) Why is that, and (2) any suggestions on how to fix it? :)

In your code you are only specifying the height of the rectangle within the card. This isn't an issue if only the rectangle is present. But:
The trouble here is that List wraps its own scrolling behavior, so it expands to take up all the space it can. Rather than capping only the rectangle inside card, cap the height of your card to currentHeight:
func card() -> some View {
ZStack(alignment: .top){
RoundedRectangle(cornerRadius: 16.0)
VStack{
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.frame(
width: 20,
height: 20
)
Text("Card")
.font(.title)
.fontWeight(.bold)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.padding(.top)
Text("LINE 2")
.foregroundColor(Color.white)
// Uncomment the following lines, and the slides becomes unusable
List {
Text("The Fellowship of the Ring")
Text("The Two Towers")
Text("The Return of the King")
}
}
}
.frame(maxHeight: currentHeight) // This line
}

The problem you are having is the List is a greedy view. It is taking up ALL of the space it possibly can, which throws your height out of whack. The simple solution is to put it in a frame, that is zero when the card is all the way down. Since we have those values, all we need to do is this:
List {
Text("The Fellowship of the Ring")
Text("The Two Towers")
Text("The Return of the King")
}
.frame(height: currentHeight - DraggableSlides.kMinHeight)

Related

Prevent Text View from changing width and position when updating in an HStack

I'm following a SwiftUI tutorial and have made this view that updates with data when a user drags or zooms into an image. After I detoured from the tutorial to round these strings to the nearest hundredth, I noticed this behavior where the text is moving back and forth (and kind of clipping for a second) when it's updating the values. I've tried various combinations of the .frame, lineLimit, minimumScaleFactor modifiers to no avail. The behavior I want is for the system icons to not move and the text to be left aligned against them and then the Text frames should take up all the available space left (and not clip into the text when the text goes from 4 characters long to 5 characters long)
Current Behavior:
InfoPanelView.swift:
...
struct InfoPanelView: View {
var scale: CGFloat
var offset: CGSize
#State private var isInfoPanelVisible: Bool = false
var body: some View {
HStack {
Image(systemName: "circle.circle")
.symbolRenderingMode(.hierarchical)
.resizable()
.frame(width: 30, height: 30)
.onLongPressGesture(minimumDuration: 1) {
withAnimation(.easeOut) {
isInfoPanelVisible.toggle()
}
}
Spacer()
HStack(spacing: 2) {
Image(systemName: "arrow.up.left.and.arrow.down.right")
Text(String(format: "%.2f", scale))
Spacer()
Image(systemName: "arrow.left.and.right")
Text(String(format: "%.2f", offset.width))
Spacer()
Image(systemName: "arrow.up.and.down")
Text(String(format: "%.2f", offset.height))
Spacer()
}
.font(.footnote)
.padding(8)
.background(.ultraThinMaterial)
.cornerRadius(8)
.frame(maxWidth: 420)
.opacity(isInfoPanelVisible ? 1 : 0)
Spacer()
}
}
}
...
I ended up creating a new subview:
struct ExpandingText: View {
var value: CGFloat
var body: some View {
ZStack {
Text(String(describing: (0..<500).map{letter in letter}))
.foregroundColor(Color.clear)
.lineLimit(1)
Text(String(format: "%.2f", value))
.id(value)
.transition(AnyTransition.opacity.animation(.easeInOut(duration:0)))
.multilineTextAlignment(.leading)
.lineLimit(1)
.frame(width: 75, alignment: .leading)
}
}
}
The 500 character invisible string that's limited to 1 line ensures that the ZStack takes up as much width as that text ever could (so the icons no longer move) and the id modifier combined with making the transition override duration being 0 fixes the text box clipping issue.
Edit:
Adding a larger width frame with alignment set to .leading as per xTwisteDx's suggestion makes it left aligned as well

SwiftUI: InsetGroupedListStyle List and full-width header

How to make a full-width header using List with InsetGroupedListStyle on iOS?
One way is to use negative padding (as an example in my code), but this doesn't seem like the best solution using fixed value as it may change in the future.
Is there a better way?
Example code:
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Section (header:
VStack {
Text("Header")
.foregroundColor(.white)
}
.frame( // This doesn't remove list's paddings
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.background(Color.red)
// .padding(.horizontal, -16) // This works, but fixed value is not the best solution.
.textCase(nil)
.font(.body)
) {
Text("Hello, world!")
.padding()
}
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You could set the width of the header to the width of the screen, and the padding (x2) you need could be reduced from the width. To can achieve this using a GeometryReader. For example,
// Geometry reader to get the width
GeometryReader { reader in
List {
Section (header:
VStack {
Text("Header")
.foregroundColor(.white)
}
// Setting the frame of the Header to the size of the screen
// Reducing 20 from the width, giving a padding of 10 on each side
.frame(width: reader.size.width - 20, height: 20, alignment: .leading)
.background(Color.red)
.textCase(nil)
.font(.body)
) {
Text("Hello, world!")
.padding()
}
}
.listStyle(InsetGroupedListStyle())
}
You can see the result below
you could try this:
...
{
Text("Hello, world!").padding()
}.headerProminence(.increased) // <--- here

Unable to change background color of view in SwiftUI

I am trying to change background color main this view but unable to do it. I tried to put background(Color.green) at HStack, VSTack and even on ZStack but it did not work, not sure if i am putting at right place. By default it is taking phone or simulator color which is white but i want to apply custom background color
My Xcode version is 11.5
struct HomePageView: View {
#State var size = UIScreen.main.bounds.width / 1.6
var body: some View {
GeometryReader{_ in
VStack {
HStack {
ZStack{
// main home page components here....
NavigationView{
VStack {
AssignmentDaysView()
}.background(Color.lairBackgroundGray)
.frame(width: 100, height: 100, alignment: .top)
.navigationBarItems(leading: Button(action: {
self.size = 10
}, label: {
Image("menu")
.resizable()
.frame(width: 30, height: 30)
}).foregroundColor(.appHeadingColor), trailing:
Button(action: {
print("profile is pressed")
}) {
HStack {
NavigationLink(destination: ProfileView()) {
LinearGradient.lairHorizontalDark
.frame(width: 30, height: 30)
.mask(
Image(systemName: "person.crop.circle")
.resizable()
.scaledToFit()
)
}
}
}
).navigationBarTitle("Home", displayMode: .inline)
}
HStack{
menu(size: self.$size)
.cornerRadius(20)
.padding(.leading, -self.size)
.offset(x: -self.size)
Spacer()
}
Spacer()
}.animation(.spring()).background(Color.lairBackgroundGray)
Spacer()
}
}
}
}
}
struct HomePageView_Previews: PreviewProvider {
static var previews: some View {
HomePageView()
}
}
In your NavigationView you have a VStack. Instead you can use a ZStack and add a background below your VStack.
Try the following:
NavigationView {
ZStack {
Color.green // <- or any other Color/Gradient/View you want
.edgesIgnoringSafeArea(.all) // <- optionally, if you want to cover the whole screen
VStack {
Text("assignments")
}
.background(Color.gray)
.frame(width: 100, height: 100, alignment: .top)
}
}
Note: you use many stacks wrapped in a GeometryReader which you don't use. Consider simplifying your View by removing unnecessary stacks. Also you may not need a GeometryReader if you use UIScreen.main.bounds (however, GeometryReader is preferred in SwiftUI).
Try removing some layers: you can start with removing the top ones: GeometryReader, VStack, HStack...
Try the following:
Change the view background color especially safe area also
struct SignUpView: View {
var body: some View {
ZStack {
Color.blue //background color
}.edgesIgnoringSafeArea(.all)

Alignment changes after adding a GeometryReader

I had a layout that essentially looked like this:
ZStack(alignment: .bottom) {
GeometryReader { geometry in
ZStack {
Text("Centered")
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
.background(Color.red)
}
Group {
GeometryReader { geometry in // This GeometryReader is causing issues.
VStack {
Text("I want this at the bottom")
}
.frame(width: geometry.size.width, height: nil, alignment: .topLeading)
}
}
}
When this is rendered, both Text elements are rendered in the center of the screen. The second text element's container takes up the entire width of the screen, which is intended. If I remove the problematic GeometryReader, then the text is properly rendered at the bottom of the screen, but obviously the frame is not set to the entire width of the screen. Why is this happening?
By default SwiftUI containers tight to content, but GeometryReader consumes maximum of available space. So if to remove second GeometryReader the VStack just wraps internal Text.
If it is still needed to keep second GeometryReader (to read width) and put text to the bottom, the simplest approach would be to add Spacer as below
Group {
GeometryReader { geometry in
VStack {
Spacer()
Text("I want this at the bottom")
}
.frame(width: geometry.size.width, height: nil, alignment: .topLeading)
}
}
Alternate approach of how to stick view at bottom you can find in my answer in this post Position view bottom without using a spacer
How about this?
struct ContentView: View {
var body: some View {
ZStack(alignment: .bottom) {
GeometryReader {geometry in
Text("Centered")
.frame(width: geometry.size.width, height: geometry.size.height)
.background(Color.red)
}
WidthReader {w in
Text("I want this at the bottom").frame(width: w)
}
}
}
}
struct WidthReader<Content: View>: View {
let widthContent: (CGFloat) -> Content
#State private var width: CGFloat = 0
#State private var height: CGFloat = 0
var body: some View {
GeometryReader {g in
widthContent(width).background(
GeometryReader {g1 in
Spacer().onAppear {height = g1.size.height}.onChange(of: g1.size.height, perform: {height = $0})
}
).onAppear {width = g.size.width}.onChange(of: g.size.width, perform: {width = $0})
}.frame(height: height)
}
}
The easiest way is to add the .fixedSize() modifier to your Stack.

SwiftUI: Pin to the top & bottom of a centered element

Using SwiftUI, I am trying to center a View on the screen and then give it a header and/or footer of variable heights.
Using constraints it would look something like this:
let view = ...
let header = ...
let footer = ...
view.centerInParent()
header.pinBottomToTop(of: view)
footer.pinTopToBottom(of: view)
This way, the view would always be centered on the screen, regardless of the size of the header and footer.
I cannot figure out how to accomplish this with SwiftUI. Using any type of HStack or VStack means the sizes of the header and footer push around the view. I would like to avoid hardcoding any heights since the center view may vary in size as well.
Any ideas? New to SwiftUI so advice is appreciated!
If I correctly understood your goal (because, as #nayem commented, at first time seems I missed), the following approach should be helpful.
Code snapshot:
extension VerticalAlignment {
private enum CenteredMiddleView: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
return dimensions[VerticalAlignment.center]
}
}
static let centeredMiddleView = VerticalAlignment(CenteredMiddleView.self)
}
extension Alignment {
static let centeredView = Alignment(horizontal: HorizontalAlignment.center,
vertical: VerticalAlignment.centeredMiddleView)
}
struct TestHeaderFooter: View {
var body: some View {
ZStack(alignment: .centeredView) {
Rectangle().fill(Color.clear) // !! Extends ZStack to full screen
VStack {
Header()
Text("I'm on center")
.alignmentGuide(.centeredMiddleView) {
$0[VerticalAlignment.center]
}
Footer()
}
}
// .edgesIgnoringSafeArea(.top) // uncomment if needed
}
}
struct Header: View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 40)
}
}
struct Footer: View {
var body: some View {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
TestHeaderFooter()
}
}
Here's the code:
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
Rectangle()
.fill(Color.gray)
.frame(width: geometry.size.width, height: geometry.size.height * 0.1, alignment: .center)
Text("Center")
.frame(width: geometry.size.width, height: geometry.size.height * 0.2, alignment: .center)
Rectangle()
.fill(Color.gray)
.frame(width: geometry.size.width, height: geometry.size.height * 0.1, alignment: .center)
}
}
}
}
using GeometryReader you can apply the dynamic size for your views.
also here is screenshot for above code
put Spacer() between header view and footer view.
headerview()
Spacer()
footerview()

Resources