I have a List within a NavigationView where each view under List should have navigatable elements attached to it (cover image, user avatar + name, etc.) For example, clicking the cover image navigates to view A, while clicking the user's name/avatar navigates to view B. Sadly, in all cases, the entire list element was clickable and did not grant the intended behavior.
At first, I tried wrapping my content within a NavigationLink.
NavigationLink(destination: Text("Media"), tag: .media, selection: $selection) {
WebImage(url: URL(string: activity.media?.coverImage?.extraLarge ?? ""))
.resizable()
.placeholder { color }
.cornerRadius(8)
.frame(width: 90, height: 135)
}
This causes an arrow to appear to indicate the view is navigatable for the user but is unwanted in this situation. It was also taking up a lot of space from the view unnecessarily.
My next attempt was to wrap the view and NavigationLink in a ZStack.
ZStack {
NavigationLink(destination: Text("Media"), tag: .media, selection: $selection) {
EmptyView()
}.hidden()
WebImage(url: URL(string: activity.media?.coverImage?.extraLarge ?? ""))
.resizable()
.placeholder { color }
.cornerRadius(8)
}.frame(width: 90, height: 135)
The .hidden() modifier was applied to the NavigationLink to prevent the arrow from appearing when the image was transparent. While this solution both hides the arrow and cleans up the extra space, there are two issues:
The entire list element is still clickable.
A ZStack covered by the .frame modifier requires I know how large I want to make it. The user's name & avatar view can't easily overcome this dilemma.
Thirdly, I tried wrapping the view in a Button where the label was the cover image and the action was to change selection to navigate programmatically, but this brought the spacing issue from #1 and the overall issue of the list element being clickable.
I later discovered a solution that would cut down the previous issues I had, but brought one problem. To understand it, this is what my main activity view looks like:
NavigationView {
List(viewModel.activities) { activity in
ActivitySelectionView(activity: activity, selection: $selection)
}.navigationTitle("Activity Feed")
}.onAppear {
viewModel.fetchActivities()
}
By encapsulating List(...) {...} in a ScrollView and changing List to a ForEach, I was able to produce the output I wanted: clickable view within an element, the cover image became lighter when clicking on it, opposed to the list element becoming darker as a whole until let go, etc.
However, this is not a list. It does not look good, nor will it look better on other platforms (this is an iOS project). For example, this code does not respect the edges as a list does. It also does not include a divider, but the Divider struct can help. I feel this is not the right solution to this problem.
To sum it all up, how do I create a List inside a NavigationView where the list respects what views inside an element are navigatable?
I found an elegant solution to my problem, so I'd like to share it for people who may stumble upon this question in the future.
You need to use a ScrollView within the List {...} somewhere. In the ScrollView block, it's perfectly suitable to make certain elements in the list cell navigatable.
NavigationView {
List(1..<11) { num in
ScrollView {
Text("\(num)!")
NavigationLink(destination: Text("Number: \(num)")) {
Text("Click me")
}
}
}
}
Related
I have a a Text("string") view in SwiftUI that displays two different strings, one much longer than the other. I would like my SwiftUI text view to present both strings as if they were equal in length, so the view remains the same size (I have other dynamic elements on the page that I do not want it affecting).
To handle this, I am padding my shorter string with spaces (" ") until it reaches the length of the larger string. This does not solve my issue - it seems like SwiftUI Text view is ignoring the trailing whitespace and not presenting it on the screen. Is there any way to resolve this?
If you are OK with having the texts occupying the whole width of the parent view, or if you can use .frame(width:) to have a fixed width, you could try the following approach:
VStack {
HStack {
Text("Short")
Spacer()
}
.background(.blue)
HStack {
Text("A very, very long text to show")
Spacer()
}
.background(.yellow)
}
.padding()
You'll see that the blue and yellow areas are the same size.
I am trying to make a "reusable" template for views in my app. As part of this I started prototyping this:
var body: some View {
NavigationView {
ZStack {
VStack {
// Spacer()
Image("progress_bar")
.resizable()
.scaledToFit()
.foregroundColor(Color.gray)
.background(Color.green)
HStack{
}
Spacer()
}
VStack{
}
}
}
.navigationBarHidden(true)
}
}
The ZStack contains 2 VStack. First one is my template and will be part of multiple of my screens later on. Second Stack is destined to be replaced by #ViewBuilder parameter so that I can reuse that in multiple screens easily.
The progress_bar image is a SVG file imported into assets, preserving Vector Data and rendered as template (So I can change colour).
My issue, as shown on the following screenshot, is that the image somehow extends toward the top of the screen. The green area correspond to the green coloured background added to the image. The progress bar is the grey line across the screen.
progress bar extending toward top of the screen
If I change my code to (commented out the spacer):
// Spacer()
Image("progress_bar")
.resizable()
.scaledToFit()
.foregroundColor(Color.gray)
.background(Color.green)
HStack{
}
Spacer()
}
I get this, progress bar shifts down in the screen (not wanted but expected) but the green area that was added on top of the image disappears:
updated screen with progress_bar shifted down and not over extending
I did try setting up a maxHeight to my Image view but this did not work out.
What am I missing? Is there a way I can stop this from happening?
Edit:
After more looking around, my issue is coming from the fact that the whole thing is embedded in a NavigationView. Apparently space is saved for the navigation bar even though it is hidden.
Question:
I'm struggling to layout views effectively with SwiftUI.
I am very familiar with UIKit and Autolayout and always found it intuitive.
I know SwiftUI is young and only beginning so maybe I expect too much, but taking a simple example:
Say I have a HStack of Text() views.
|--------------------------------|
| Text("static") Text("Dynamic") |
|________________________________|
When I have dynamic content, the static Text strings jump all over the place as the size of the HStack changes, when Text("Dynamic") changes...
I've tried lot's of things, Spacers(), Dividers(), looked at approaches using PreferenceKeys (link), Alignment Guides (link)
Closest to an answer seems alignment guides, but they are convoluted.
What's the canonical approach to replicate Autolayout's ability to basically anchor views to near the edge of the screen, and layout correctly without jumping around?
I'd like to anchor the static text "Latitude" so it doesn't jump around.
There are other examples, so a more general answer on how best to layout would be appreciated...
With Autolayout it felt I chose were things went. With SwiftUI it's a lottery.
Example, showing the word "Latitude" jump around as co-ordinates change:
Example, code:
HStack {
Text("Latitude:")
Text(verbatim: "\(self.viewModelContext.lastRecordedLocation().coordinate.latitude)")
}
I'm really struggling when my views have changing/dynamic context. All works OK for static content as shown in all of the WWDC videos.
Potential Solution:
Using a HStack like this:
HStack(alignment: .center, spacing: 20) {
Text("Latitude:")
Text(verbatim: "\(self.viewModelContext.lastRecordedLocation().coordinate.latitude)")
Spacer()
}
.padding(90)
The result is nicely anchored, but I hate magic numbers.
As you've somewhat discovered, the first piece is that you need to decide what you want. In this case, you seem to want left-alignment (based on your padding solution). So that's good:
HStack {
Text("Latitude:")
Text(verbatim: "\(randomNumber)")
Spacer()
}
That's going to make the HStack as wide as its containing view and push the text to the left.
But from you later comments, you seem to not want it to be on the far left. You have to decide exactly what you want in that case. Adding .padding will let you move it in from the left (perhaps by adding .leading only), but maybe you want to match it to the screen size.
Here's one way to do that. The important thing is to remember the basic algorithm for HStack, which is to give everyone their minimum, and then split up the remaining space among flexible views.
HStack {
HStack {
Spacer()
Text("Latitude:")
}
HStack {
Text(verbatim: "\(randomNumber)")
Spacer()
}
}
The outer HStack has 2 children, all of whom are flexible down to some minimum, so it offers each an equal amount of space (1/2 of the total width) if it can fit that.
(I originally did this with 2 extra Spacers, but I forgot the Spacers seem to have special handling to get their space last.)
The question is what happens if randomNumber is too long? As written, it'll wrap. Alternatively, you could add .fixedSize() which would stop it from wrapping (and push Latitude to the left to make it fit). Or you could add .lineLimit(1) to force it to truncate. It's up to you.
But the important thing is the addition of flexible HStacks. If every child is flexible, then they all get the same space.
If you want to force things into thirds or quarters, I find you need to add something other than a Spacer. For example, this will give Latitude and the number 1/4 of the available space rather than 1/2 (note the addition of Text("")):
HStack {
HStack {
Text("")
Spacer()
}
HStack {
Spacer()
Text("Latitude:")
}
HStack {
Text(verbatim: "\(randomNumber)")//.lineLimit(1)
Spacer()
}
HStack {
Text("")
Spacer()
}
}
In my own code, I do this kind of thing so much I have things like
struct RowView: View {
// A centered column
func Column<V: View>(#ViewBuilder content: () -> V) -> some View {
HStack {
Spacer()
content()
Spacer()
}
}
var body: some View {
HStack {
Column { Text("Name") }
Column { Text("Street") }
Column { Text("City") }
}
}
}
I've added a link to the trailing edge of my navbar using the following code:
bodyContent
.navigationBarItems(trailing:
NavigationLink(
destination: InfoView()
) {
Image(systemName: "info.circle")
}
)
This code works, however I'm finding that it's difficult for the user to press (testing on an iPhone X) due to a small selectable area.
Using the view debugger, I believe this small rectangle within the circular icon is the selectable area:
How can I increase the size of this area, at least so that it is as large as the circular icon? Ideally the user would be able to press anywhere above and below the icon as well, within the vertical space occupied by the navbar.
Just add padding
bodyContent
.navigationBarItems(trailing:
NavigationLink(
destination: InfoView()
) {
Image(systemName: "info.circle").padding() // << here !!
}
)
You can also pass a
.frame(width: 100, height: 100)
and it will be clickable in the entire space, though other UI elements will act as if your icon is that size.
I am developing an app based on a Tabview with three TabItems. Each TabItem is a List and I would be able to show a kind of modal view over those Lists. The problem becomes when I can not call a Sheet as modal view because Sheets are almost full windowed. I need some kind of bottom modal view, so I create a View that I present over a List with higher ZIndex. It seems to work until you click in the tabbar and select another TabItem having deployed the "modal" view. The error is:
[TableView] Warning once only: UITableView was told to layout its
visible cells and other contents without being in the view hierarchy
(the table view or one of its superviews has not been added to a
window). This may cause bugs by forcing views inside the table view to
load and perform layout without accurate information (e.g. table view
bounds, trait collection, layout margins, safe area insets, etc), and
will also cause unnecessary performance overhead due to extra layout
passes.
So, I would like as solution to restrict the tappable area to the "modal" view area. ¿Is there a way to achieve this?
Probably you have some condition state depending on which you present your "modal-like" view, so depending on the same condition you can disable below TabView, like below
TabView {
// ... tabs content here
}.disabled(showingModal)
Update: Here is a demo of approach that I meant (tested with Xcode 11.3+)
struct TestTabViewModal: View {
#State private var selectedTab = 0
#State private var modalShown = false
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
VStack {
Button("Show Modal") { self.modalShown = true }
.padding(.top, 40)
Spacer()
}
.tabItem {
Image(systemName: "1.circle")
}.tag(0)
Text("2").tabItem {
Image(systemName: "1.circle")
}.tag(1)
}.disabled(modalShown)
if modalShown {
RoundedRectangle(cornerRadius: 10)
.fill(Color.yellow)
.frame(width: 320, height: 240)
.overlay(Button("CloseMe") { self.modalShown = false })
}
}
}
}