ForEach inside ScrollView doesn't take whole width - ios

I'm trying to re-create UI of my current app using SwiftUI. And it is way more difficult than I initially though.
I wanted to achieve card-like cells with some background behind them. I found that List doesn't support that, at least yet. List is so limited - it doesn't allow you to remove cell separator.
So I moved to ForEach inside ScrollView. I guess that isn't something which should be used in production for long tables but that should work for now. The problem I have is that ForeEach view doesn't take all the width ScrollView provides. I can set .frame(...) modifier but that will require hardcoding width which I definitely don't want to do.
Any ideas how to force VStack take full width of the ScrollView? I tried to use ForeEach without VStack and it has the same issue. It seems like ScrollView (parent view) "tells" its child view (VStack) that its frame is less that actual ScrollView's frame. And based on that information child views build their layout and sizes.
Here is my current result:
And here is the code:
struct LandmarkList : View {
var body: some View {
NavigationView {
ScrollView() {
VStack {
Spacer().frame(height: 160)
ForEach(landmarkData) { landmark in
LandmarkRow(landmark: landmark).padding([.leading, .trailing], 16)
}
}.scaledToFill()
.background(Color.pink)
}
.background(Color.yellow)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkRow : View {
var landmark: Landmark
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(landmark.name).font(.title)
Text("Subtitle")
.font(.callout)
.color(.gray)
}
Spacer()
Text("5 mi")
.font(.largeTitle)
}.frame(height: 80)
.padding()
.background(Color.white)
.cornerRadius(16)
.clipped()
.shadow(radius: 2)
}
}

I've got the same issue, the only way I have found so far is to fix the ScrollView and the content view width, so that every subview you add inside the content view will be centered.
I created a simple wrapper that take the width as init parameter
struct CenteredList<Data: RandomAccessCollection, Content: View>: View where Data.Element: Identifiable {
public private(set) var width: Length
private var data: Data
private var contentBuilder: (Data.Element.IdentifiedValue) -> Content
init(
width: Length = UIScreen.main.bounds.width,
data: Data,
#ViewBuilder content: #escaping (Data.Element.IdentifiedValue) -> Content)
{
self.width = width
self.data = data
self.contentBuilder = content
}
var body: some View {
ScrollView {
VStack {
ForEach(data) { item in
return self.contentBuilder(item)
}.frame(width: width)
}
.frame(width: width)
}
.frame(width: width)
}
}
By default it takes the screen width (UIScreen.main.bounds.width).
It works just like a List view:
var body: some View {
TileList(data: 0...3) { index in
HStack {
Text("Hello world")
Text("#\(index)")
}
}
}

Its possible that the answer to this might just be wrapping your scrollView inside of a GeometryReader
Like done in the answer here -> How do I stretch a View to its parent frame with SwiftUI?

Related

I want to create a view where the hashtag is displayed in SwiftUI

Currently, I am studying SwiftUI and making UI that is displayed in the view when registering hashtags.
But I don't know what to do with the logic of creating and inserting a new HStack when it's out of screen size inside the VStack.
I've searched several times, but I couldn't find any helpful words or keywords.
I would appreciate it if you could help me by knowing the answer.
The image above is an example.
I want to create a new HStack and put it in the VStack when the width exceeds the horizontal size of the device while inserting a text item into the HStack.
struct HashTagView: View {
var hashTagArray: [String] = ["#Lorem", "#Ipsum", "#dolor", "#sit", "#amet", "#consectetur", "#adipiscing", "#elit", "#Nam", "#semper", "#sit", "#amet", "#ut", "#eleifend", "#Cras"]
var body: some View {
VStack(spacing: 5) {
ForEach(hashTagArray, id:\.self) { tag in
Text(tag)
}
}
.padding()
.border(Color.blue)
.frame(width: UIScreen.main.bounds.size.width)
}
}
this is my code.
You should use a LazyVgrid with an adaptive layout here:
I removed some entries from your array because if you use it with id: \.self you should ensure that every entry is unique.
Documentation
struct HashTagView: View {
var hashTagArray: [String] = ["#Lorem", "#Ipsum", "#dolor", "#consectetur", "#adipiscing", "#elit", "#Nam", "#semper", "#sit", "#amet", "#ut", "#eleifend", "#Cras"]
private var gridItemLayout = [GridItem(.adaptive(minimum: 100))]
var body: some View {
ScrollView{
LazyVGrid(columns: gridItemLayout , spacing: 5) {
ForEach(hashTagArray, id:\.self) { tag in
Text(tag)
}
}
.padding()
.border(Color.blue)
}
.frame(width: UIScreen.main.bounds.size.width)
}
}

How to get Text width with SwiftUI?

I would like to underline a title with a rectangle that should have the same width as the Text.
First I create an underlined text as below:
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}
}
}
So I get the following result:
Now I want to get this result:
So I would like to know if it's possible to bind Text width and apply it to Rectangle by writing something like :
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(width: Text.width, height: (5.0))
}
}
}
By doing so, I could change text and it will be dynamically underlined with correct width.
I tried many options but I can't find how to do it. I also checked this question but it's seems to not be the same issue.
Just specify that container has fixed size and it will tight to content, like
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}.fixedSize() // << here !!
}

SwiftUI- can't drag a view outside of a ScrollView

Here is a very simplified version of my layout. I have a text view that I'm able to drag around...
struct DragView: View {
var text:String
#State var dragAmt = CGSize.zero
var body: some View {
Text(text)
.padding(15)
.background(Color.orange)
.cornerRadius(20)
.padding(5)
.frame(width: 150, height: 60, alignment: .leading)
.offset(dragAmt)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged {
self.dragAmt = CGSize(width: $0.translation.width, height: $0.translation.height)
}
.onEnded {_ in
self.dragAmt = CGSize.zero
})
}
}
And then I arrange these views like so:
struct ContentView: View {
var body: some View {
HStack {
ScrollView {
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
Divider()
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
}
}
The views that are not in the ScrollView act as expected, and I can drag them around the entire screen. The views that are in the ScrollView, however, will disappear if brought outside the bounds of the ScrollView.
Why does this occur? Is there any way to enable these views to be dragged outside the ScrollView?
Why does this occur?
You drag, you just don't see it because ScrollView clips its content view. Stacks do not, by default. If you add .clipped() for bottom VStack you'll see the same.
Is there any way to enable these views to be dragged outside the
ScrollView?
Solution might be using ZStack. Say on drag you hide one view in ScrollView and show same out-of ScrollView at the same global location, so visually it would behave as would drag original.

Issues with SwiftUI List NavigationLink frame(height: CGFloat)

I am trying to display a custom view for my List Row, it is an HStack with an image, a title Text and another Text containing a short paragraph. I am having issues with the height of the NavigationLink item. I have tried setting the frame to a specific height but it does not display optimally on different devices (the small resolution difference between the iPhone XR and iPhone XS is enough to make me have to adjust the height manually).
Is there a way to have the frame height be set automatically? I have tried many things such as setting the frame height on my PlanetRow View, adding padding, setting the lineLimit to: 0, 3, 5, ... Whatever I do, if I don't set the height for the NavigationLink View, the PlanetRow extends past the NavigationLink View (visible beyond the List separators).
Here's the code for my views:
struct PlanetRow : View {
var planet: Planet
var body: some View {
HStack {
Image("Mars")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text(planet.name)
.font(.largeTitle)
Text(planet.overview)
.font(.caption)
}
.lineLimit(nil)
}
}
}
struct PlanetList : View {
var body: some View {
NavigationView {
List(planetData.identified(by: \.id)) { planet in
NavigationLink(destination: PlanetDetail(planet: planet)) {
PlanetRow(planet: planet)
}
.frame(height: 150)
}
.navigationBarTitle(Text("Planets"))
}
}
}

SwiftUI Login Page Layout

I am exploring SwiftUI as I am trying to build a login view and now I am facing a problem
This is what I am trying to achieve:
As you can see I already reached this point but I don't like my implementation
struct ContentView : View {
#State var username: String = ""
var body: some View {
VStack(alignment: .leading) {
Text("Login")
.font(.title)
.multilineTextAlignment(.center)
.lineLimit(nil)
Text("Please")
.font(.subheadline)
HStack {
VStack (alignment: .leading, spacing: 20) {
Text("Username: ")
Text("Password: ")
}
VStack {
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
}
}
}.padding()
}
}
Because in order to make the username and password text aligned exactly in the middle of the textfield, I had to put literal spacing value of 20 in the VStack which I don't like because most probably It won't work on different device sizes.
Anyone sees a better way to achieve the same result?
Thanks
We're going to implement two new View modifier methods so that we can write this:
struct ContentView: View {
#State var labelWidth: CGFloat? = nil
#State var username = ""
#State var password = ""
var body: some View {
VStack {
HStack {
Text("User:")
.equalSizedLabel(width: labelWidth, alignment: .trailing)
TextField("User", text: $username)
}
HStack {
Text("Password:")
.equalSizedLabel(width: labelWidth, alignment: .trailing)
SecureField("Password", text: $password)
}
}
.padding()
.textFieldStyle(.roundedBorder)
.storeMaxLabelWidth(in: $labelWidth)
}
}
The two new modifiers are equalSizedLabel(width:alignment:) and storeMaxLabelWidth(in:).
The equalSizedLabel(width:alignment) modifier does two things:
It applies the width and alignment to its content (the Text(“User:”) and Text(“Password:”) views).
It measures the width of its content and passes that up to any ancestor view that wants it.
The storeMaxLabelWidth(in:) modifier receives those widths measured by equalSizedLabel and stores the maximum width in the $labelWidth binding we pass to it.
So, how do we implement these modifiers? How do we pass a value from a descendant view up to an ancestor? In SwiftUI, we do this using the (currently undocumented) “preference” system.
To define a new preference, we define a type conforming to PreferenceKey. To conform to PreferenceKey, we have to define the default value for our preference, and we have to define how to combine the preferences of multiple subviews. We want our preference to be the maximum width of all the labels, so the default value is zero and we combine preferences by taking the maximum. Here's the PreferenceKey we'll use:
struct MaxLabelWidth: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = max(value, nextValue())
}
}
The preference modifier function sets a preference, so we can say .preference(key: MaxLabelWidth.self, value: width) to set our preference, but we have to know what width to set. We need to use a GeometryReader to get the width, and it's a little tricky to do properly, so we'll wrap it up in a ViewModifier like this:
extension MaxLabelWidth: ViewModifier {
func body(content: Content) -> some View {
return content
.background(GeometryReader { proxy in
Color.clear
.preference(key: Self.self, value: proxy.size.width)
})
}
}
What's happening above is we attach a background View to the content, because a background is always the same size as the content it's attached to. The background View is a GeometryReader, which (via the proxy) provides access to its own size. We have to give the GeometryReader its own content. Since we don't actually want to show a background behind the original content, we use Color.clear as the GeometryReader's content. Finally, we use the preference modifier to store the width as the MaxLabelWidth preference.
Now have can define the equalSizedLabel(width:alignment:) and storeMaxLabelWidth(in:) modifier methods:
extension View {
func equalSizedLabel(width: CGFloat?, alignment: Alignment) -> some View {
return self
.modifier(MaxLabelWidth())
.frame(width: width, alignment: alignment)
}
}
extension View {
func storeMaxLabelWidth(in binding: Binding<CGFloat?>) -> some View {
return self.onPreferenceChange(MaxLabelWidth.self) {
binding.value = $0
}
}
}
Here's the result:
You can use Spacers alongside with fixedSize modifier for height. You should set set heights of any row's object in order to achieve exact table style view:
struct ContentView : View {
private let height: Length = 32
#State var username: String = ""
var body: some View {
VStack(alignment: .leading) {
Text("Login")
.font(.title)
.multilineTextAlignment(.center)
.lineLimit(nil)
Text("Please")
.font(.subheadline)
HStack {
VStack (alignment: .leading) {
Text("Username: ") .frame(height: height)
Spacer()
Text("Password: ") .frame(height: height)
}
VStack {
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
.frame(height: height)
Spacer()
TextField($username, placeholder: Text("type something here..."))
.textFieldStyle(.roundedBorder)
.frame(height: height)
}
}
.fixedSize(horizontal: false, vertical: true)
}
.padding()
}
}
Note that setting height on TextField does not effect it's height directly, but it will just set the height of it's content text's height.
If you are looking to do something similar with Buttons on macOS, be advised that as of Xcode 11.3 you'll end up with the following at run time:
instead of:
I've written up my solution in this blog post. It is quite similar to #rob-mayoff 's answer and works for both labels and buttons.

Resources