I'm creating a small SwiftUI app with a TabView, but the code stopped working as soon as I used the modifier .tabViewStyle(PageTabViewStyle()). Why is that? The app is meant to cycle through a series of images and is going to be similar in design to the Photos app. It's based on a Kavsoft tutorial, but that tutorial used deprecated code. When I used the more modern equivalent, the app's preview stopped working, and I can no longer see the TabView.
struct Home: View {
/* Posts */
#State var posts: [Post] = []
/* Cuurent Image */
#State var currentImage = ""
var body: some View {
/* Gallery */
TabView(selection: $currentImage) {
ForEach(posts) { post in
/* GeometryReader to get screen sizes for image */
GeometryReader { geo in
let size = geo.size
Image(post.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height)
.cornerRadius(0)
}
.edgesIgnoringSafeArea(.all)
}
}
.tabViewStyle(PageTabViewStyle()) // Cannot work
.onAppear {
for index in 1...10 {
posts.append(Post(image: "\(index)"))
}
}
}
}
struct Post: Identifiable, Hashable {
var id = UUID().uuidString
var image: String
}
Adding an answer here for completeness, I suspected it would work with static content like Text(). This was crashing because ForEach is trying to load posts that don't exist yet, causing it to init from an empty array. For the tab view to work correctly, it needs at least one rendered view.
To fix this, posts needs to have at least one element in it to render.
Related
The Problem
In our brand new SwiftUI project (Multiplatform app for iPhone, iPad and Mac) we are using a ScrollView with both .horizontal and .vertical axes enabled. In the end it all looks like a spreadsheet.
Inside of ScrollView we are using LazyVStack with pinnedViews: [.sectionHeaders, .sectionFooters]. All Footer, Header and the content cells are wrapped into a LazyHStack each. The lazy stacks are needed due to performance reasones when rows and columns are growing.
And this exactly seems to be the problem. ScrollView produces a real layout mess when scrolling in both axes at the same time.
Tested Remedies
I tried several workarounds with no success.
We built our own lazy loading horizontal stack view which shows only the cells whose indexes are in the visible frame of the scrollview, and which sets dynamic calculated leading and trailing padding.
I tried the Introspect for SwiftUI packacke to set usesPredominantAxisScrolling for macOS and isDirectionalLockEnabled for iOS. None of this works as expected.
I also tried a workaround to manually lock one axis when the other one is scrolling. This works fine on iOS but it doesn't on macOS due to extreme poor performance (I think it's caused by the missing NSScrollViewDelegate and scroll events are being propagated via NotificationCenter).
How does it look?
To give you a better idea of what I mean, I have screenshots for both iOS and macOS.
Here you can watch short screen recording for both iOS and macOS.
Example Code
And this is the sample code used for the screenshots. So you can just create a new Multiplatform project and paste the code below in the ContentView.swift file. That's all.
import SwiftUI
let numberOfColums: Int = 150
let numberOfRows: Int = 350
struct ContentView: View {
var items: [[Item]] = {
var items: [[Item]] = [[]]
for rowIndex in 0..<numberOfRows {
var row: [Item] = []
for columnIndex in 0..<numberOfColums {
row.append(Item(column: columnIndex, row: rowIndex))
}
items.append(row)
}
return items
}()
var body: some View {
Group {
ScrollView([.horizontal, .vertical]) {
LazyVStack(alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders, .sectionFooters]) {
Section(header: Header()) {
ForEach(0..<items.count, id: \.self) { rowIndex in
let row = items[rowIndex]
LazyHStack(spacing: 1) {
ForEach(0..<row.count, id: \.self) { columnIndex in
Text("\(columnIndex):\(rowIndex)")
.frame(width: 100)
.padding()
.background(Color.gray.opacity(0.2))
}
}
}
}
}
}
.edgesIgnoringSafeArea([.top])
}
.padding(.top, 1)
}
}
struct Header: View {
var body: some View {
LazyHStack(spacing: 1) {
ForEach(0..<numberOfColums, id: \.self) { idx in
Text("Col \(idx)")
.frame(width: 100)
.padding()
.background(Color.gray)
}
Spacer()
}
}
}
struct Item: Hashable {
let id: String = UUID().uuidString
var column: Int
var row: Int
}
Can You Help?
Okay, long story short...
Does anybody know a real working solution for this issue?
Is it a known SwiftUI ScrollView bug?
If there were a way to lock one scrolling direction while the other one is being scrolled, then I could deal with that. But at the moment, unfortunately, it is not usable for us.
So any solution is highly appreciated! 😃
Is there any reason you don't want to use a Grid for this layout?
Here is the article I would suggest you read to solve this problem:
https://swiftui-lab.com/impossible-grids/
I have an interesting situation in regards to animations in SwiftUI. In its simplified form, I have a view that shows a rectangle which, when tapped, should toggle a Bool binding and represent the change with an animated transition of color. But it seems like the animation doesn't happen when the view is inside a NavigationView and the binding is coming from a StateObject instead of simple local state. I can't explain why that would be the case, and would appreciate any thoughts.
Below is the code that shows a simplified case that reproduces the issue. The app code isn't particularly interesting; it's the default code that creates a WindowGroup and shows an instance of ContentView in it.
import SwiftUI
class AppState: ObservableObject {
#Published var isRed = false
}
struct RectView: View {
#Binding var isRed: Bool
var body: some View {
Rectangle()
.fill(isRed ? Color.red : Color.gray)
.frame(width: 75, height: 75, alignment: .center)
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) {
isRed.toggle()
}
}
}
}
struct ContentView: View {
#StateObject var appState = AppState()
#State private var childViewIsRed = false
var body: some View {
VStack {
NavigationView {
List {
NavigationLink("Link with binding to state object", destination: RectView(isRed: $appState.isRed))
NavigationLink("Link with binding to state variable", destination: RectView(isRed: $childViewIsRed))
}
}
.frame(height: 300)
RectView(isRed: $appState.isRed)
RectView(isRed: $childViewIsRed)
}
}
}
The gif/video below is me demonstrating four things, tapping on these views from bottom to top:
First I tap on the very bottom rectangle - the one with a binding to a #State property. It toggles with animation as expected. I tap again to leave it gray.
Then I tap the second rectangle from the bottom - one with a binding to a #Published property in the #StateObject. All is well.
Next, I tap on the NavigationLink that leads to a rectangle that is bound to the local #State property. When I tap on the rectangle the transition is animated fine. The very bottom rectangle also animates, which makes sense since they are bound to the same property.
Finally I tap on the top NavigationLink, which leads to a rectangle bound to the #Published property in the #StateObject. When I tap on this rectangle though, there is no animation. The rectangle snaps to red. The rectangle below it (which is bound to the same property) animates fine, proving that the property is indeed toggled. But there is no animation inside the NavigationView. Why? What am I missing?
I've searched for existing questions. I'm aware that there are some around NavigationView (like this) but that doesn't explain why one type of binding would work fine inside a NavigationView and another wouldn't. Similarly, there are ones around animating changes to #ObservedObjects (like this, would a #StateObject be similar?) but I don't follow why animating changes to such a binding would work fine outside of a NavigatonView and not inside one.
In case it's relevant I'm using Xcode 13.2.1, running on macOS 12.2.1, which is in turn running on a 16-inch M1 Max MacBook Pro. The simulator shown in the gif/video is an iPhone 11 Pro Max. I get the same results if I deploy this tiny test app to a physical iPhone 7 running iOS 15.3.1.
To be honest with you, I am not sure why, but utilizing the animation modifier on the RectView allows the animation to occur without any issues.
struct RectView: View {
#Binding var isRed: Bool
var body: some View {
Rectangle()
.fill(isRed ? Color.red : Color.gray)
.frame(width: 75, height: 75, alignment: .center)
.animation(.easeOut(duration: 1), value: isRed)
.onTapGesture { isRed.toggle() }
}
}
screen recording example
I have built a scrollview that is populated after an API call is successful. The issue i am facing is after the first time content is populated all the listings are crammed onto the center of the screen. It is only after i close the view and reopen it that i see the proper listings properly fit on the screen. I have no idea, i've tried to readjust the frame with no luck.
The fact that when i navigate away and back then the view corrects itself is what's confusing me, had it been maintaining the same behavior it would be easily understandable
ScrollView(.vertical, showsIndicators: false){
if(searchResultsViewModel.listings.count == 0){
ActivityIndicator().foregroundColor(Color(hex:Global.colorAccent))
.frame(width: 30, height: 30).padding(EdgeInsets(top: 15,leading: 0,bottom: 0,trailing: 0))
}else {
ForEach(self.searchResultsViewModel.listings, id: \.self){ item in
HStack(spacing: 0) {
ForEach(item, id: \.self) { listing in
NavigationLink(destination: ListingDetailsView(listing: listing)) {
ListingView(listing: listing)
}.buttonStyle(PlainButtonStyle())
}
}
}
}
}.navigationBarTitle(title)
Below is an image of what im trying to explain
The solution is to force rebuild scrollview, because original does not know that content size changed.
So assuming self.searchResultsViewModel is observed view model
#State private var scrollViewID = UUID() // << initial
// ... other code
ScrollView(.vertical, showsIndicators: false){
// ... content here
}.id(scrollViewID) // << dynamic id
.navigationBarTitle(title)
.onReceive(self.searchResultsViewModel.$listings) { _ in
self.scrollViewID = UUID() // << force rebuild
}
My app has one button that produces either a green or red background. The user presses the button over and over to generate green or red each time and the results are recorded into an Observable Object called "Current Session." The current totals are displayed in a custom view made of capsules. For some reason, the view will not update the totals and keeps displaying 0.
I've tried adding a state binding for numberOfGreens in ResultsBar but that doesn't work either. It still just shows 0 every time. The environment has been added to all of my views. I've tried printing the results and the result numbers show up correct in the console but the UI will not update. Interestingly, the trialsRemaining variable works perfectly on other screens in my app, having no trouble updating when it has to. Also, ResultsBar works perfectly fine in preview.
class CurrentSession: ObservableObject {
#Published var trialsRemaining: Int = 100
#Published var numberOfGreens: Int = 0
#Published var numberOfReds: Int = 0
func generateGreenOrRed() {
...
if resultIsGreen {numberOfGreens += 1}
else if resultIsRed {numberOfReds += 1}
trialsRemaining -= 1
}
}
struct ResultsBar: View {
let totalBarWidth: CGFloat = UIScreen.main.bounds.width - 48
var onePercentOfTotalWidth: CGFloat { return totalBarWidth/100 }
var numberOfGreens: Int
var body: some View {
ZStack(alignment: .leading) {
Capsule()
.frame(width: totalBarWidth, height: 36)
.foregroundColor(.red)
.shadow(radius: 4)
Capsule()
.frame(width: (onePercentOfTotalWidth * CGFloat(numberOfGreens)), height: 36)
.foregroundColor(.green)
.shadow(radius: 4)
}
}
}
struct ResultsView: View {
#EnvironmentObject var session: CurrentSession
var body: some View {
VStack {
HStack {
Text("\(session.numberOfGreens)")
Spacer()
Text("\(session.numberOfReds)")
}
ResultsBar(numberOfGreens: session.numberOfGreens)
}
}
}
I am running Xcode 11.2 beta 2 (11B44) on macOS Catalina 10.15.2 Beta (19C32e).
Are you passing a reference to a CurrentSession instance to your ResultsView? If I copy your code, the following does update the ResultsBar when I click on it.
struct ContentView: View {
#ObservedObject var session = CurrentSession()
var body: some View {
ResultsView()
.environmentObject(session)
.onTapGesture {
self.session.generateGreenOrRed()
}
}
}
I had this same issue, but my issue was because I was updating the environment object on a background thread because I'm using an api. In order to fix my issue I just needed to wrapped setting my EnvironmentObject vars like the following.
DispatchQueue.main.async{
self.numberOfGreens = GetNumberOfGreens(id: id)
}
I hope this helps someone.
For some reason, putting a GeometryReader as an intermediary, kills geometry of its nested views if it's a List "cell".
Example code:
struct SampleView: View {
var multilineText: some View {
Text(
"""
Some
Amazing
Multiline
Copy
"""
)
}
var body: some View {
List(1...5, id: \.self) { _ in
GeometryReader { _ in
self.multilineText
}
}
}
}
Without GeometryReader (Expected) / Actual with GeometryReader:
This example, obviously, is over-simplified, but there is a legitimate reason to measure geometry for a nested view I'm building.
This is on Xcode 11 beta 6. Should I go straight into reporting this as a bug, or it's something expected and workable around?
Add min row height for the list.
List(1...5, id: \.self) { _ in
GeometryReader { _ in
self.multilineText
}
}.environment(\.defaultMinListRowHeight, 100)