How to stop the page from being able to be scrolled up and down while using a paged tab view in swiftUI - swift

I am trying to build an app that shows different days of the week in a paged tab view, but when I scroll sideways to a day (e.g. Tuesday), I can scroll it up and down as if it was a scroll view. I don't have a scroll view in my content view.
My code is something like this:
struct ContentView: View {
var body: some View {
TabView {
Text("Saturday")
Text("Sunday")
Text("Monday")
Text("Tuesday")
Text("Wednesday")
Text("Thursday")
Text("Friday")
}
.tabViewStyle(PageTabViewStyle())
}
}

You can do that like this
Put TabView inside the ScrollView with .onAppear()
.onAppear(perform: {
UIScrollView.appearance().alwaysBounceVertical = false
})
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
TabView {
Text("Saturday")
Text("Sunday")
Text("Monday")
Text("Tuesday")
Text("Wednesday")
Text("Thursday")
Text("Friday")
}
.tabViewStyle(PageTabViewStyle())
.frame(width: 300, height: 600, alignment: .center)
}
.frame(width: 300, height: 600, alignment: .center)
.background(Color.blue)
.onAppear(perform: {
UIScrollView.appearance().alwaysBounceVertical = false
})
}
}

Related

How to create a view that functions like Alert in SwiftUI on watchOS?

I want to have an alert style view that can include an icon image at the top along with the title.
alert(isPresented:content:) used with Alert does not seem to support adding images in any way. However, other than that limitation, it functions as I want my alert to function. It covers the status bar and blurs the main view as the background for the alert.
I attempted to use fullScreenCover(isPresented:onDismiss:content:) instead, but this approach does not behave like an alert as far as covering up EVERYTHING including the status bar, and blurring the main view as the background.
I have tried this so far, with the following result. One of the reasons I want it to behave like a normal Alert is so that the content can scroll without overlapping the clock time.
struct AlertView: View {
#EnvironmentObject var dataProvider: DataProvider
var alert: Watch.AlertMessage
var body: some View {
ScrollView {
Image("IconAlert")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.foregroundColor(.accentColor)
Text("Test Title")
.bold()
Text("Test Description")
Button("DISMISS") { print("dismiss") }
}
.padding()
.ignoresSafeArea()
.navigationBarHidden(true)
}
}
Try this this sample custom alert. It may give you some great approach(Code is below the image):
struct CustomAlert: View {
#State var isClicked = false
var body: some View {
ZStack {
Color.orange.edgesIgnoringSafeArea(.all)
Button("Show Alert") {
withAnimation {
isClicked.toggle()
}
}
ZStack {
if isClicked {
RoundedRectangle(cornerRadius: 20)
.fill(.thickMaterial)
.frame(width: 250, height: 250)
.transition(.scale)
VStack {
Image("Swift")
.resizable()
.frame(width: 150, height: 150)
.mask(RoundedRectangle(cornerRadius: 15))
Button("Dismiss") {
withAnimation {
isClicked.toggle()
}
}
.foregroundColor(.red)
}
.transition(.scale)
}
}
}
}
}

NavigationViews in TabView displayed incorrectly

I'm using SwiftUI and want to build a paged TabView with two NavigationView pages I can switch between horizontally. The pictures below show how it's supposed to work:
Here is my code for above example:
struct ContentView: View {
var body: some View {
TabView {
Page1()
Page2()
}
.tabViewStyle(.page)
}}
struct Page1: View {
var body: some View {
NavigationView {
ScrollView {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
}
.navigationTitle("Page 1")
}
.navigationViewStyle(StackNavigationViewStyle())
}}
struct Page2: View {
var body: some View {
NavigationView {
ScrollView {
Rectangle()
.fill(.blue)
.frame(width: 100, height: 100)
}
.navigationTitle("Page 2")
}
.navigationViewStyle(StackNavigationViewStyle())
}}
Unless there's another way to do it is important that the NavigationViews are inside the TabView so that if I'm scrolling up the navigation bar title switches to .inline like you can see below:
Also, I use the StackNavigationViewStyle() because without it the pages aren't shown when I first open the app before I switch back and forth between them.
StackNavigationViewStyle() solves this but still the problem is that when I open the app for the first time the rectangles are being placed incorrectly right at the top of the screen. I then have to switch to the second page and back to get them positioned correctly:
Does anyone have an idea?
One solution is to use the Tab selection parameter. It is better not to use the StackNavigationViewStyle() with these embedded NavigationViews. What you may use is a selecter, which keeps track of the page you are on and a State variable storing the current page. This way, the NavigationView is in one place and different titels are given to each TabView item.
struct ContentView: View {
#State private var selectedTab = "1"
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
Page1()
.tag("1")
.navigationBarTitle("Page 1")
Page2()
.tag("2")
.navigationBarTitle("Page 2")
}
.tabViewStyle(.page)
}
}
}
struct Page1: View {
var body: some View {
ScrollView {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
}
}}
struct Page2: View {
var body: some View {
ScrollView {
Rectangle()
.fill(.blue)
.frame(width: 100, height: 100)
}
}}
Updated
When you want the NavigationTitle to be .inline when scrolled, use the following code.
struct ContentView: View {
#State private var selectedTab = "1"
var body: some View {
NavigationView {
GeometryReader { proxy in
ScrollView(showsIndicators: false) {
TabView(selection: $selectedTab) {
Page1()
.tag("1")
.navigationBarTitle("Page 1")
Page2()
.tag("2")
.navigationBarTitle("Page 2")
}
.tabViewStyle(.page)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(height: proxy.size.height)
.ignoresSafeArea()
}
}
}
}
}
struct Page1: View {
var body: some View {
VStack {
Rectangle()
.fill(.red)
.frame(width: 200, height: 200)
}
}
}
struct Page2: View {
var body: some View {
VStack {
Rectangle()
.fill(.blue)
.frame(width: 200, height: 200)
}
}
}

Swift UI Navigation View not switching the whole screen

I want to create a simple NavigationView. But with code outside of it. Like this:
struct TestView: View {
var body: some View {
VStack{
RoundedRectangle(cornerRadius: 10)
.frame(width: 100, height: 100)
NavigationView {
VStack{
NavigationLink {
Text("HEllo")
} label: {
Text("Click me")
}
.navigationViewStyle(.columns)
}
.navigationTitle("A Title")
}
}
}
}
I do that so the navigation Title is below the item outside the NavigationView.
This code gives me this:
Image because I am not allowed to insert images yet.
When I click on the NavigationLink though I see this:
The Image
As you see the RoundedRectangle still is viewable at top of the screen. How can I fix that, so that the Rectangle disappears and the Destination is viewable in full screen?
Set navigation view first, You have to put everything inside the navigation view.
NavigationView { // Here
VStack{
RoundedRectangle(cornerRadius: 10)
.frame(width: 100, height: 100)
VStack{ // Remove from above
The RoundedRectangle is still visible because it is outside of the NavigationView, only the content of the NavigationView will move with the NavigationLink
Something you could do is to use the toolbar of the NavigationView to place items on the top of the screen
struct TestView: View {
var body: some View {
NavigationView {
NavigationLink {
Text("HEllo")
} label: {
Text("Click me")
}
.navigationViewStyle(.columns)
.navigationTitle("A Title")
.toolbar {
ToolbarItem {
RoundedRectangle(cornerRadius: 10)
.frame(width: 100, height: 100)
}
}
}
}
}

ScrollViewReader doesn't scroll to the correct position in a 2D ScrollView

Here is a simple SwiftUI code that builds a large grid which is put inside a 2D ScrollView. Clicking each item will force the ScrollView to scroll to another item whose id is 50 more than the clicked item's. The ScrollView did scroll but it never scrolled to the correct one. I tried it on macOS and iOS, and neither worked. I also tried using VStack and HStack instead of LazyVGrid, and still it didn't work. What is the correct way to achieve this?
struct ContentView: View {
let columns = Array.init(repeating: GridItem(.fixed(80)), count: 20)
var body: some View {
ScrollView([.horizontal, .vertical]) {
ScrollViewReader { proxy in
LazyVGrid(columns: columns, spacing: 20) {
ForEach(0..<500) { i in
ZStack {
Circle()
.fill(Color.green)
.frame(width: 30, height: 30)
Text("\(i)")
}
.id("\(i)")
.onTapGesture {
proxy.scrollTo("\(i + 50)")
}
}
}
.padding(.horizontal)
}
}
.frame(minWidth: 500, minHeight: 300)
}
}

ScrollView + NavigationView animation glitch SwiftUI

I've got a simple view:
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(0..<2) { _ in
CardVew(for: cardData)
}
}
}
.navigationBarTitle("Testing", displayMode: .automatic)
}
}
But you can replace the CardView with anything - the glitch presists. Glitch video
Is there a way to fix it?
Xcode 12.0.1, Swift 5
Setting the top padding to 1 is breaking at least 2 major things:
The scroll view does not extend under NavigationView and TabView - this making it loose the beautiful blur effect of the content that scrolls under the bars.
Setting background on the scroll view will cause Large Title NavigationView to stop collapsing.
I've encountered these issues when i had to change the background color on all screens of the app i was working on.
So i did a little bit more digging and experimenting and managed to figure out a pretty nice solution to the problem.
Here is the raw solution:
We wrap the ScrollView into 2 geometry readers.
The top one is respecting the safe area - we need this one in order to read the safe area insets
The second is going full screen.
We put the scroll view into the second geometry reader - making it size to full screen.
Then we add the content using VStack, by applying safe area paddings.
At the end - we have scroll view that does not flicker and accepts background without breaking the large title of the navigation bar.
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geometryWithSafeArea in
GeometryReader { geometry in
ScrollView {
VStack {
Color.red.frame(width: 100, height: 100, alignment: .center)
ForEach(0..<5) { i in
Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)
Spacer()
}
Color.red.frame(width: 100, height: 100, alignment: .center)
}
.padding(.top, geometryWithSafeArea.safeAreaInsets.top)
.padding(.bottom, geometryWithSafeArea.safeAreaInsets.bottom)
.padding(.leading, geometryWithSafeArea.safeAreaInsets.leading)
.padding(.trailing, geometryWithSafeArea.safeAreaInsets.trailing)
}
.background(Color.yellow)
}
.edgesIgnoringSafeArea(.all)
}
.navigationBarTitle(Text("Example"))
}
}
}
The elegant solution
Since the solution is clear now - lets create an elegant solution that can be reused and applied to any existing ScrollView by just replacing the padding fix.
We create an extension of ScrollView that declares the fixFlickering function.
The logic is basically we wrap the receiver into the geometry readers and wrap its content into the VStack with the safe area paddings - that's it.
The ScrollView is used, because the compiler incorrectly infers the Content of the nested scroll view as should being the same as the receiver. Declaring AnyView explicitly will make it accept the wrapped content.
There are 2 overloads:
the first one does not accept any arguments and you can just call it on any of your existing scroll views, eg. you can replace the .padding(.top, 1) with .fixFlickering() - thats it.
the second one accept a configurator closure, which is used to give you the chance to setup the nested scroll view. Thats needed because we don't use the receiver and just wrap it, but we create a new instance of ScrollView and use only the receiver's configuration and content. In this closure you can modify the provided ScrollView in any way you would like, eg. setting a background color.
extension ScrollView {
public func fixFlickering() -> some View {
return self.fixFlickering { (scrollView) in
return scrollView
}
}
public func fixFlickering<T: View>(#ViewBuilder configurator: #escaping (ScrollView<AnyView>) -> T) -> some View {
GeometryReader { geometryWithSafeArea in
GeometryReader { geometry in
configurator(
ScrollView<AnyView>(self.axes, showsIndicators: self.showsIndicators) {
AnyView(
VStack {
self.content
}
.padding(.top, geometryWithSafeArea.safeAreaInsets.top)
.padding(.bottom, geometryWithSafeArea.safeAreaInsets.bottom)
.padding(.leading, geometryWithSafeArea.safeAreaInsets.leading)
.padding(.trailing, geometryWithSafeArea.safeAreaInsets.trailing)
)
}
)
}
.edgesIgnoringSafeArea(.all)
}
}
}
Example 1
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
Color.red.frame(width: 100, height: 100, alignment: .center)
ForEach(0..<5) { i in
Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)
Spacer()
}
Color.red.frame(width: 100, height: 100, alignment: .center)
}
}
.fixFlickering { scrollView in
scrollView
.background(Color.yellow)
}
.navigationBarTitle(Text("Example"))
}
}
}
Example 2
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
Color.red.frame(width: 100, height: 100, alignment: .center)
ForEach(0..<5) { i in
Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)
Spacer()
}
Color.red.frame(width: 100, height: 100, alignment: .center)
}
}
.fixFlickering()
.navigationBarTitle(Text("Example"))
}
}
}
Here's a workaround. Add .padding(.top, 1) to the ScrollView:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(0..<2) { _ in
Color.blue.frame(width: 350, height: 200)
}
}
}
.padding(.top, 1)
.navigationBarTitle("Testing", displayMode: .automatic)
}
}
}
I simplified #KoCMoHaBTa's answer. Here it is:
extension ScrollView {
private typealias PaddedContent = ModifiedContent<Content, _PaddingLayout>
func fixFlickering() -> some View {
GeometryReader { geo in
ScrollView<PaddedContent>(axes, showsIndicators: showsIndicators) {
content.padding(geo.safeAreaInsets) as! PaddedContent
}
.edgesIgnoringSafeArea(.all)
}
}
}
Use like so:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
/* ... */
}
.fixFlickering()
}
}
}
while facing the same problem I did a bit of investigation.
The glitch seems to come from a combination of scrollview bouncing, and the speed of deceleration of the scrolling context.
For now I have managed to make the glitch disappear by settings the deceleration rate to fast. It seem to let swiftui better compute the layout while keeping the bounce animation active.
My work around is as simple as to set the following in the init of your view. The drawback its that this affects the speed of your scrolling deceleration.
init() {
UIScrollView.appearance().decelerationRate = .fast
}
A possible improvement would be to compute the size of the content being displayed and then switch on the fly the deceleration rate depending what would be needed.