SwiftUI DisclosureGroup won't expand inside LazyVStack - ios

I discovered a problem that in some case, inside LazyVStack, especially when the content is long, DisclosureGroup won't expand when tapped. Is this a SwiftUI bug or did I do this the wrong way?
Below is an example reproducing the problem (Xcode 14.0.1, iOS 16.0.3), notice that the last 7 DisclosureGroup won't expand.
import SwiftUI
struct Entity: Identifiable {
var id = UUID()
let header = "HEADER"
let body = "BODY"
}
struct FoldView: View {
var entities: [Entity]
init() {
entities = []
for _ in 1...30 {
entities.append(Entity())
}
}
var body: some View {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(entities) { entity in
DisclosureGroup {
Text(entity.body)
} label: {
Text(entity.header)
}
Text("Middle")
}
}
.padding(.horizontal)
}
}
}
struct FoldView_Previews: PreviewProvider {
static var previews: some View {
FoldView()
}
}

Try this, I just removed Text("Middle")
var body: some View {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(entities) { entity in
DisclosureGroup {
Text(entity.body)
} label: {
Text(entity.header)
}
}
}
.padding(.horizontal)
}
}

Related

iOS 16 SwiftUI TextField in section header (List)

If run this code on iOS16 keyboard gets dismissed randomly when character is typed (please see gif), while on iOS15 everything is fine.
struct ContentView: View {
let names = ["Holly", "Josh", "Rhonda", "Ted"]
#State var text = ""
var body: some View {
List {
Section {
ForEach(searchResults, id: \.self) { name in
Text(name)
}
} header: {
TextField("Search for name", text: $text)
}
}
}
var searchResults: [String] {
if text.isEmpty {
return names
} else {
return names.filter { $0.contains(text) }
}
}
}
It happens when content is in a section with a header. Is it bug from apple introduced in iOS16 or am I doing something wrong? Has anyone had the same issue?
It might have something to do with the way List works. I experimented a bit and if you add .searchable to the Section instead of the List, I am not able to reproduce the problem.
struct ContentView: View {
let names = ["Holly", "Josh", "Rhonda", "Ted"]
#State var text = ""
var body: some View {
List {
Section {
ForEach(searchResults, id: \.self) { name in
Text(name)
}
} header: {
TextField("Search for name", text: $text)
}.searchable(text: $text) // <- Here
}
}
var searchResults: [String] {
if text.isEmpty {
return names
} else {
return names.filter { $0.contains(text) }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Searchable adds it's own textfield, you shouldn't add another one, especially not one in a section that is being removed/added.

SwiftUI: Transparent NavBar in 3 Column Layout on iPhone

I have a 3 column layout and for some reason, the navigation bar in the middle column starts off buggy on the initial launch of the app, ie transparent and unresponsive. I have to navigate away and back in order for the navigation bar to function properly. This only happens when I run it on the iPhone simulator, iPad works fine.
Code:
struct ContentView: View {
var body: some View {
NavigationView {
Sidebar()
MainView()
Text("Select mail to read")
}
}
}
struct Sidebar: View {
var body: some View {
List {
Section(header: Text("Browse")
) {
NavigationLink(destination: MainView()) {
Text("Inbox")
}
NavigationLink(destination: SentView()) {
Text("Sent")
}
}
}
.listStyle(SidebarListStyle())
.navigationTitle("Mailboxes")
}
}
struct MainView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
Group {
if viewModel.isFetching {
ProgressView()
} else {
List {
ForEach(0..<20) { i in
NavigationLink(
destination: DetailView(item: "\(i)"),
label: {
Text("Inbox \(i)")
})
}
}
}
}
.navigationTitle("Inbox")
.listStyle(PlainListStyle())
}
}
struct SentView: View {
var body: some View {
List {
ForEach(0..<5) { i in
NavigationLink(
destination: DetailView(item: "\(i)"),
label: {
Text("Sent \(i)")
})
}
}
.navigationTitle("Sent")
.listStyle(PlainListStyle())
}
}
class ViewModel: ObservableObject {
#Published var isFetching = false
init() {
fetchData()
}
func fetchData() {
isFetching = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
self.isFetching = false
}
}
}
When running on iPhone:
When running on iPad:
EDIT:
Replaced code and gif with a more generic implementation for brevity.

SwiftUI iOS 14 View does not Update #Published Array with #EnvironmentObject

I'm working on a calorie-tracker app.
In my App, I can open the Detail side of some products, set the amount and add the product to the "Cart".
Later, I want to read out all collected datas from the array and show them an a short overview.
But this View won't be updated after making changer on the array.
Due to I storage the datas with the userDefaults, I always have to reopen the app to update the view. Only then, the hole array will be displayed.
My Class Cart:
import Foundation
struct Order: Equatable, Identifiable, Codable {
var id = UUID()
var product: Product
var modifier: Double
var date: Date
}
class Cart: ObservableObject {
#Published var orders = [Order]()
static let saveKey = "SavedData"
init() {
if let data = UserDefaults.standard.data(forKey: Self.saveKey) {
if let decoded = try? JSONDecoder().decode([Order].self, from: data) {
self.orders = decoded
}
} else {
self.orders = []
}
}
// save order
func save() {
if let encoded = try? JSONEncoder().encode(self.orders) {
UserDefaults.standard.set(encoded, forKey: Self.saveKey)
}
}
// add to order
func add(order: Order) {
self.orders.append(order)
print("product added to cart")
save()
}
// remove from order
func remove(order: Order) {
if let index = orders.firstIndex(of: order) {
orders.remove(at: index)
}
}
}
I made a View to apply the amount of any special product.
import SwiftUI
struct AmountView: View {
#EnvironmentObject var cart: Cart
#State private var textInput = ""
#State private var orderFinished = false
var product: Product
func StringDoubleConverter(text: String) -> String {
return String(format: "%.2f", Double(textInput.replacingOccurrences(of: ",", with: ".")) ?? 0)
}
var body: some View {
VStack {
Form {
Section(header: Text("Mengenangabe")) {
// input for the amount
AmountInputView(textInput: $textInput)
if !orderFinished {
Button("Hinzufügen", action: {
orderFinished = true
hideKeyboard()
// add product to the cart
self.cart.add(order: Order(product: product, modifier: Double(StringDoubleConverter(text: textInput))!, date: Date()))
})
.disabled(textInput == "")
.animation(.default)
} else {
Text("Wurde zum Logbuch hinzugefügt")
.foregroundColor(.blue)
}
}
productNutritionCollectionView(product: product, modifier: Double(StringDoubleConverter(text: textInput))!)
}
}
}
}
struct AmountView_Previews: PreviewProvider {
static var previews: some View {
AmountView(product: Product.exampleProduct).environmentObject(Cart())
}
}
Then, I want to display all products in the order in a logbook view using a Form and a ForEach lope.
struct LogbookView: View {
func deleteProducts(at offsets: IndexSet) {
cart.orders.remove(atOffsets: offsets)
cart.save()
}
#EnvironmentObject var cart: Cart
#State private var date = Date()
var body: some View {
NavigationView {
Form {
Section(header: Text("List")) {
ForEach(cart.orders) { order in
Text(order.product.name)
}
.onDelete(perform: { indexSet in
deleteProducts(at: indexSet)
})
}
}
.navigationBarTitle(Text("Logbuch"), displayMode: .automatic)
.navigationBarItems(trailing: DateView(date: $date))
}
}
}
struct LogbookView_Previews: PreviewProvider {
static var previews: some View {
LogbookView().environmentObject(Cart())
}
}
I'm using a AppTab View to navigate the app. Therefore, I changed the AppTab View in the main Struct to the default View with an environment object of Cart.
#main
struct KalorientrackerApp: App {
var body: some Scene {
WindowGroup {
AppTabView().environmentObject(Cart())
}
}
}
struct KalorientrackerApp_Previews: PreviewProvider {
static var previews: some View {
Text("Hello, World!")
}
}
I'm opening my AmountView using a .sheet
struct ProductDetailView: View {
#State private var showAmountView = false
let product: Product
var body: some View {
VStack {
// placeholder Image
Image(product.fullImage)
.clipShape(Circle())
.padding(.top, 5)
Spacer()
Form {
productNutritionCollectionView(product: product, modifier: 100)
}
}
// Titel for Navigation bar
.navigationBarTitle(Text(product.name), displayMode: .inline)
// Button to go to amount view
.navigationBarItems(trailing: Button(action: {
self.showAmountView = true
}, label: {
Image(systemName: "plus.circle")
.padding(.leading, 20)
}).sheet(isPresented: $showAmountView, content: {
AmountView(product: product).environmentObject(Cart())
}))
}
}
struct ProductDetailView_Previews: PreviewProvider {
static var previews: some View {
ProductDetailView(product: Product.exampleProduct) }
}
I already found some other discussions, but they didn't worked for me.
I'm using Xcode 12 beta 6 and iOS14 beta 6
I found the bug myself. The problem was, that I committed explicit an .environmentObject in my .sheet action.
AmountView(product: product).environmentObject(Cart())
I removed .environmentObject(Cart()) from the .sheet action. Now it's working.
Thinking this caused the bug because I'm using the .environmentObject(Cart()) operator in the main View of my project.

Making a combine passthrough publisher less global

Swift 5, iOS 13
I want to use passthroughSubject publisher; but I my gut tells me its a global variable and as such very poor practice. How can make this global variable less global, while still being usable. Here's some code to show what I talking about.
I know there are a dozen other ways to do this, but I wanted to create some simple code to illustrate the issue.
import SwiftUI
import Combine
let switcher = PassthroughSubject<Void,Never>()
struct SwiftUIViewF: View {
#State var nextPage = false
var body: some View {
VStack {
Text("Switcher")
.onReceive(switcher) { (_) in
self.nextPage.toggle()
}
if nextPage {
Page1ViewF()
} else {
Page2ViewF()
}
}
}
}
struct Page1ViewF: View {
var body: some View {
Text("Page 1")
.onTapGesture {
switcher.send()
}
}
}
struct Page2ViewF: View {
var body: some View {
Text("Page 2")
.onTapGesture {
switcher.send()
}
}
}
struct SwiftUIViewF_Previews: PreviewProvider {
static var previews: some View {
SwiftUIViewF()
}
}
Here is possible solution - to hold it in parent and inject into child views:
struct SwiftUIViewF: View {
let switcher = PassthroughSubject<Void,Never>()
#State var nextPage = false
var body: some View {
VStack {
Text("Switcher")
.onReceive(switcher) { (_) in
self.nextPage.toggle()
}
if nextPage {
Page1ViewF(switcher: switcher)
} else {
Page2ViewF(switcher: switcher)
}
}
}
}
struct Page1ViewF: View {
let switcher: PassthroughSubject<Void,Never>
var body: some View {
Text("Page 1")
.onTapGesture {
self.switcher.send()
}
}
}
struct Page2ViewF: View {
let switcher: PassthroughSubject<Void,Never>
var body: some View {
Text("Page 2")
.onTapGesture {
self.switcher.send()
}
}
}
An example using #EnvironmentObject.
Let SDK take care of observing / passing things for you, rather than setting up yourself.
Especially when your usage is a simple toggle.
import SwiftUI
import Combine
final class EnvState: ObservableObject { #Published var nextPage = false }
struct SwiftUIViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
VStack {
Text("Switcher")
if env.nextPage {
Page1ViewF()
} else {
Page2ViewF()
}
}
}
}
struct Page1ViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
Text("Page 1")
.onTapGesture {
env.nextPage.toggle()
}
}
}
struct Page2ViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
Text("Page 2")
.onTapGesture {
env.nextPage.toggle()
}
}
}
struct SwiftUIViewF_Previews: PreviewProvider {
static var previews: some View {
SwiftUIViewF().environmentObject(EnvState())
}
}

How to switch to another view by each element's onTapGesture of a list in SwiftUI?

I tried to add a navigation view in the list as following. But it not works saying Result of 'NavigationView<Content>' initializer is unused
var body: some View {
GeometryReader { geometry in
VStack {
List {
ForEach(self.allItems){ item in
TaskRow(item: item)
.onTapGesture {
// TODO: switch to another view
NavigationView {
VStack {
Text("Hello World")
NavigationLink(destination: AnotherView()) {
Text("Do Something")
}
}
}
}
}
}
}
}
}
And AnotherView is a SwiftUI file as following:
import SwiftUI
struct AnotherView: View {
var body: some View {
VStack{
Text("Hello, World!")
}
}
}
struct AnotherView_Previews: PreviewProvider {
static var previews: some View {
AnotherView()
}
}
I have tried the solution in stackoverflow Switching Views With Observable Objects in SwiftUI and SwiftUI Change View with Button. They neither work in my situation.
How to switch to another view by onTapGesture of the list in SwiftUI like following:
var body: some View {
GeometryReader { geometry in
VStack {
List {
ForEach(self.allItems){ item in
TaskRow(item: item)
.onTapGesture {
// TODO: switch to another view
AnotherView()
}
}
}
}
}
}
You have to place whole your body into NavigationView.
Example
struct Item: Identifiable {
let id = UUID()
let name: String
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach([Item(name: "A"), Item(name: "B")]) { value in
NavigationLink(destination: X(item: value)) {
Text(value.name)
}
}
}
}
}
}
struct X: View {
let item: Item
var body: some View {
Text(item.name)
}
}

Resources