Why LazyHStack takes full height but HStack does not in SwiftUI? - swiftui

Why does a LazyHStack behaves differently than an HStack regarding the height? (same for the VStack).
import SwiftUI
struct LazyTestView: View {
var body: some View {
LazyHStack {
ForEach(1...10, id: \.self) { int in
Text("\(int)")
}
}
}
}
struct LazyTestView_Previews: PreviewProvider {
static var previews: some View {
LazyTestView()
.previewLayout(.sizeThatFits)
}
}
Whereas with an HStack:
import SwiftUI
struct LazyTestView: View {
var body: some View {
HStack {
ForEach(1...10, id: \.self) { int in
Text("\(int)")
}
}
}
}
One solution is to add .fixedSized() for the LazyHStack...
PS: Xcode Version 12.5 beta (12E5220o)

It was from beginning like that. Started with LazyVStack, the reason is because Lazy Stacks does not know all possible content that they are carrying, there for it takes safer approach to be prepared for any size of content, in the other hand the normal Stacks does know exactly what are their children and therefore they take a frame or size that they really needed for that, not more not less!

Related

Does a LazyVStack remain "lazy" if it's inside a VStack?

Let's consider a list of 100 posts. According to Apple, if I layout them inside a LazyVStack:
the stack view doesn’t create items until it needs to render them
onscreen.
What if I embed that LazyVStack inside a VStack? Does it still load the views "as needed"?
struct MyView: View {
init() {
print("init...")
}
var body: some View {
Text("test")
}
}
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
LazyVStack {
ForEach.init(0..<100) { int in
MyView()
}
}
}
}
}
}
Running the above code, as we scroll we can see more MyViews are init'd (by viewing the print statements in the console), so it seems like a LazyVStack in a VStack does indeed create it's content lazily. We can see the same is true when removing the VStack as well.

NavigationStack not affected by EnvironmentObject changes

I'm attempting to use #EnvironmentObject to pass an #Published navigation path into a SwiftUI NavigationStack using a simple wrapper ObservableObject, and the code builds without issue, but working with the #EnvironmentObject has no effect. Here's a simplified example that still exhibits the issue:
import SwiftUI
class NavigationCoordinator: ObservableObject {
#Published var path = NavigationPath()
func popToRoot() {
path.removeLast(path.count)
}
}
struct ContentView: View {
#StateObject var navigationCoordinator = NavigationCoordinator()
var body: some View {
NavigationStack(path: $navigationCoordinator.path, root: {
FirstView()
})
.environmentObject(navigationCoordinator)
}
}
struct FirstView: View {
var body: some View {
VStack {
NavigationLink(destination: SecondView()) {
Text("Go To SecondView")
}
}
.navigationTitle(Text("FirstView"))
}
}
struct SecondView: View {
var body: some View {
VStack {
NavigationLink(destination: ThirdView()) {
Text("Go To ThirdView")
}
}
.navigationTitle(Text("SecondView"))
}
}
struct ThirdView: View {
#EnvironmentObject var navigationCoordinator: NavigationCoordinator
var body: some View {
VStack {
Button("Pop to FirstView") {
navigationCoordinator.popToRoot()
}
}
.navigationTitle(Text("ThirdView"))
}
}
I am:
Passing the path into the NavigationStack path parameter
Sending the simple ObservableObject instance into the NavigationStack via the .environmentObject() modifier
Pushing a few simple child views onto the stack
Attempting to use the environment object in ThirdView
NOT crashing when attempting to use the environment object (e.g. "No ObservableObject of type NavigationCoordinator found")
Am I missing anything else that would prevent the deeply stacked view from using the EnvironmentObject to affect the NavigationStack's path? It seems like the NavigationStack just isn't respecting the bound path.
(iOS 16.0, Xcode 14.0)
The reason your code is not working is that you haven't added anything to your path, so your path is empty. You can simply verify this by adding print(path.count) in your popToRoot method it will print 0 in the console.
To work with NavigationPath you need to use navigationDestination(for:destination:) ViewModifier, So for your example, you can try something like this.
ContentView:- Change NavigationStack like this.
NavigationStack(path: $navigationCoordinator.path) {
VStack {
NavigationLink(value: 1) {
Text("Go To SecondView")
}
}
.navigationDestination(for: Int.self) { i in
if i == 1 {
SecondView()
}
else {
ThirdView()
}
}
}
SecondView:- Change NavigationLink like this.
NavigationLink(value: 2) {
Text("Go To ThirdView")
}
This workaround works with Int but is not a better approach, so my suggestion is to use a custom Array as a path. Like this.
enum AppView {
case second, third
}
class NavigationCoordinator: ObservableObject {
#Published var path = [AppView]()
}
NavigationStack(path: $navigationCoordinator.path) {
FirstView()
.navigationDestination(for: AppView.self) { path in
switch path {
case .second: SecondView()
case .third: ThirdView()
}
}
}
Now change NavigationLink in FirstView and SecondView like this.
NavigationLink(value: AppView.second) {
Text("Go To SecondView")
}
NavigationLink(value: AppView.third) {
Text("Go To ThirdView")
}
The benefit of the above is now you can use the button as well to push a new screen and just need to append in your path.
path.append(.second)
//OR
path.append(.third)
This will push a respected view.
For more details, you can read the Apple document of NavigationLink and NavigationPath.

Why does this SwiftUI LazyHStack update continuously?

I have a large set of URLs to images. I display the files' thumbnails in a LazyVStack. I have wrapped up the 'ThumbnailView' and the 'ThumbnailGenerator' in a struct and class respectively. However, when I ran the code I discovered that it kept re-initaiting the ThumbnailGenerators. After some investigation I found that after removing an HStack in the main view's hierarchy the problem went away.
Any thoughts as to why this might happen. (BTW I did log this with Apple, but still feel I am doing something wrong here myself.)
I have stripped the code back to the bare essentials here, replacing the thumbnail generation code with a simple sleep statement, to demonstrate the bug in action. Run it with the HStack in and it will print out the date continuously. Take it out and it works as expected.
#main
struct ExperimentApp: App {
var body: some Scene {
WindowGroup {
LazyVIssue()
.frame(width: 200, height: 140)
.padding(100)
}
}
}
struct LazyVIssue: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<10) { i in
HStack { /// <---- REMOVE THIS HSTACK AND IT WORKS
ThumbnailView()
Text("Filename \(i)")
}.padding()
}
}
}
}
}
struct ThumbnailView: View {
#StateObject private var thumbnailGenerator : ThumbnailGenerator
init() {
_thumbnailGenerator = StateObject(wrappedValue: ThumbnailGenerator())
}
var body: some View {
thumbnailGenerator.image
}
}
final class ThumbnailGenerator: ObservableObject {
var image : Image
init() {
print("Initiating", Date())
image = Image(systemName: "questionmark.circle.fill")
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
guard let self = self else { return }
sleep(1) /// Simulate some work to fetch image
self.image = Image(systemName: "camera.circle.fill")
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
}
I'm not sure why this is happening but I've seen had some funky things happen like this as well. If you initialize the ThumbnailGenerator() outside of the ThumbnailView init, I believe the issue goes away.
init(generator: ThumbnailGenerator) {
_thumbnailGenerator = StateObject(wrappedValue: generator)
}
Well, it is not clear for now what's going on here definitely (it is something about LazyVStack caching), but there is workaround - move everything into single row view.
Tested with Xcode 12.1 / iOS 14.1
struct LazyVIssue: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(0..<10) { i in
ThumbnailView(i) // << single row view !!
}
}
}
}
}
struct ThumbnailView: View {
#StateObject private var thumbnailGenerator : ThumbnailGenerator
let row: Int
init(_ row: Int) {
self.row = row
_thumbnailGenerator = StateObject(wrappedValue: ThumbnailGenerator())
}
var body: some View {
HStack {
thumbnailGenerator.image
Text("Filename \(row)")
}.padding()
}
}

Make List Sections non-collapsible in SwiftUI when embedded into a NavigationView SwiftUI

When I embed a List grouped into Sections into a NavigationView the section headers become collapsible. I'd like to keep them non-collapsible, just like when the List is not embedded into the NavigationView.
My current code (with the NavigationView):
import SwiftUI
struct MyGroup {
var name:String, items:[String]
}
struct ContentView: View {
var groups : [MyGroup] = [
.init(name: "Animals", items: ["πŸ•","🐩","πŸ‚","πŸ„","🐈","🦩","🐿","πŸ‡"]),
.init(name: "Vehicles", items: ["πŸš•","πŸš—","πŸšƒ","πŸš‚","🚟","🚀","πŸ›₯","⛡️"])]
var body: some View {
NavigationView {
VStack {
List {
ForEach(groups, id: \.self.name) { group in
Section(header: Text(group.name)) {
ForEach(group.items, id:\.self) { item in
Text(item)
}
}
}
}
}.navigationTitle("collections")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It is default style applied, you can make it explicitly set for List like below (tested with Xcode 12 / iOS 14)
List {
ForEach(groups, id: \.self.name) { group in
Section(header: Text(group.name)) {
ForEach(group.items, id:\.self) { item in
Text(item)
}
}
}
}.listStyle(InsetGroupedListStyle()) // or GroupedListStyle
Just using the SidebarListStyle in listStyle modifier
.listStyle(SidebarListStyle())
In case you're stumbling on this... The issue doesn't have anything to do with being embedded in a NavigationView as the OP and #Danial mentioned. It's because it's embedded in the the VStack at the first level of the NavigationView in the example code. Seems like a SwiftUI bug to me.

Why does binding to the Picker not work anymore in swiftui?

When I run a Picker Code in the Simulator or the Canvas, the Picker goes always back to the first option with an animation or just freezes. This happens since last Thursday/Friday. So I checked some old simple code, where it worked before that and it doesn't work for me there, too.
This is the simple old Code. It doesn't work anymore in beta 3, 4 and 5.
struct PickerView : View {
#State var selectedOptionIndex = 0
var body: some View {
VStack {
Text("Option: \(selectedOptionIndex)")
Picker(selection: $selectedOptionIndex, label: Text("")) {
Text("Option 1")
Text("Option 2")
Text("Option 3")
}
}
}
}
In my newer code, I used #ObservedObject, but also here it doesn't work.
Also I don't get any errors and it builds and runs.
Thank you for any pointers.
----EDIT----- Please look at the answer first
After the help, that I could use the .tag() behind all Text()like Text("Option 1").tag(), it now takes the initial value and updates it inside the view. If I use #ObservedObject like here:
struct PickerView: View {
#ObservedObject var data: Model
let width: CGFloat
let height: CGFloat
var body: some View {
VStack(alignment: .leading) {
Picker(selection: $data.exercise, label: Text("select exercise")) {
ForEach(data.exercises, id: \.self) { exercise in
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
}
}
.frame(width: width, height: (height/2), alignment: .center)
}
}
}
}
Unfortunately it doesn't reflect changes on the value, if I make these changes in another view, one navigationlink further. And also it doesn't seem to work with the my code above, where I use firstIndex(of: exercise)
---EDIT---
Now the code above works if I change
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
into
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise)!)
because it couldn't work with an optional.
The answer summarized:
With the .tag() behind the Options it works. It would look like following:
Picker(selection: $selectedOptionIndex, label: Text("")) {
ForEach(1...3) { index in
Text("Option \(index)").tag(index)
}
}
If you use a range of Objects it could look like this:
Picker(selection: $data.exercises, label: Text("")) {
ForEach(0..<data.exercises.count) { index in
Text("\(data.exercises[index])").tag(index)
}
}
I am not sure if it is intended, that .tag() is needed to be used here, but it's at least a workaround.
I found a way to simplify the code a bit without the need of operating on indicies and tags.
At first, make sure to conform your model to Identifiable protocol like this (this is actually a key part, as it enables SwiftUI to differentiate elements):
public enum EditScheduleMode: String, CaseIterable, Identifiable {
case closeSchedule
case openSchedule
public var id: EditScheduleMode { self }
var localizedTitle: String { ... }
}
Then you can declare viewModel like this:
public class EditScheduleViewModel: ObservableObject {
#Published public var editScheduleMode = EditScheduleMode.closeSchedule
public let modes = EditScheduleMode.allCases
}
and UI:
struct ModeSelectionView: View {
private let elements: [EditScheduleMode]
#Binding private var selectedElement: EditScheduleMode
internal init?(elements: [EditScheduleMode],
selectedElement: Binding<EditScheduleMode>) {
self.elements = elements
_selectedElement = selectedElement
}
internal var body: some View {
VStack {
Picker("", selection: $selectedElement) {
ForEach(elements) { element in
Text(element.localizedTitle)
}
}
.pickerStyle(.segmented)
}
}
}
With all of those you can create a view like this:
ModeSelectionView(elements: viewModel.modes, selectedElement: $viewModel.editScheduleMode)