SwiftUI: Malfunctioning NavigationBar and TabBar in iOS 15 - ios

In SwiftUI, I would like to use a background color for my view while also setting navigationViewStyle to .stack, to force a single-column stack navigation Plus-sized devices.
var body: some View {
TabView {
NavigationView {
ScrollView {
ForEach(0..<100) { _ in
Text("Hello, world!")
.padding()
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Demo")
// .background(Color.yellow) // I can do this …
}
// .navigationViewStyle(.stack) // … or this, but not both!
.tabItem {
Label("Demo", systemImage: "swift")
}
}
}
However, when I do both, the navigation bar won't collapse when I scroll down. Also both the navigation bar and tab bar appear without background.
When I only set the background, but leave out the line that sets the navigationViewStyle, everything looks fine in portrait mode, or smaller devices. But on a Plus-size device in landscape, it looks like this:
So I guess I really can't do this without setting the navigationViewStyle.
What can I do to fix this? Is this a bug that should be fixed by Apple? All help is greatly appreciated.

Use the .navigationViewStyle view modifier on the ScrollView
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
ScrollView {
ForEach(0..<100) { _ in
Text("Hello, world!")
.padding()
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Demo")
.background(Color.yellow)
.navigationViewStyle(.stack)
}
.tabItem {
Label("Demo", systemImage: "swift")
}
}
}
}
Update
I guess, it is a bug. It does not work.
1
If all of your ScrollViews have the same background, use this once on a view.
struct ScrollViewBackground: ViewModifier {
let color: Color
func body(content: Content) -> some View {
content
.ignoresSafeArea(edges: .horizontal)
.onAppear {
UIScrollView.appearance().backgroundColor = UIColor(color)
}
}
}
extension View {
func setBackgroundColor(color: Color) -> some View {
return self.modifier(ScrollViewBackground(color: color))
}
}
2
Use introspect to access the underlaying UIScrollView and change its background. You need to also use .ignoresSafeArea(edges: .horizontal) on the ScrollView.
ScrollView {
Text("Item 2")
}
.introspectScrollView { scrollView in
scrollView.backgroundColor = UIColor(color.yellow)
}

Related

Add accessory view below navigation bar title in SwiftUI

I’m trying to add an accessory view embedded in a navigation bar below the title, which can be seen in the default iOS calendar app (the “s m t w t f s” row) or the GitHub mobile app:
And I’d like it to work along with the large title style navigation bar like the GH mobile.
LazyVStack’s pinnedView with a section header almost work, but I can’t get the background color to make it seemless with the navigation bar, even with the ultraThinMaterial. It also leaves the divider line between the pinned view and the bar.
Is there a way to achieve this layout?
Solutions in SwiftUI, SwiftUI+Introspect, and UIKit are all welcome!
Have you tried setting a .safeAreaInset view? This will have the stickiness you're looking for, and items in the "main" part of the view will take its height into account when rendering, so won't get obscured.
Here's a quick example I knocked up:
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(0 ..< 30) { item in
Text("Hello, world!")
}
}
.navigationTitle("Accessory View")
.safeAreaInset(edge: .top) {
AccessoryView()
}
}
}
}
struct AccessoryView: View {
var body: some View {
HStack {
Button("Button") { }
Button("Button") { }
Button("Button") { }
Spacer()
}
.padding()
.background(Color(uiColor: .systemGroupedBackground))
.buttonStyle(.bordered)
.controlSize(.mini)
}
}
You have to give the view a background otherwise it'll be transparent – but that background will (as long as it's a colour or a material) automatically extend into the navigation bar itself. Here's a GIF of the above code in action, where I've set the background to match the grouped list's background:
It's not perfect, especially as it looks distinct from the nav bar on scroll, but it might be useable for you?
Another idea is to replace the navigation bar with a custom one like this:
{
...
}
.safeAreaInset(edge: .top) {
VStack(alignment: .leading, spacing: 8) {
HStack() {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "chevron.backward")
}
Spacer()
Text(navigationTitle).font(.title2).bold()
.multilineTextAlignment(.center)
.foregroundColor(.accentColor)
.frame(maxWidth: .infinity)
Spacer()
}
HStack {
Button("Button") { }
Button("Button") { }
Button("Button") { }
Spacer()
}
}
.padding()
.background(
.bar
)
}
You will also have to set:
.navigationBarBackButtonHidden(true)
and do not set a navigation title:
// .navigationTitle("....")

Possible to allowsHitTesting on part of view?

I have a button behind a ScrollView, but cannot tap it since it's under it. I have a Spacer at the top of the scroll view that shows the button. I tried putting allowsHitTesting(false) on the Spacer, but this is still not letting it pass underneath the ScrollView.
This is what the `ScrollView looks like, I cannot tap the "Press" button:
This is the code, notice the button is in the ZStack, and the Spacer in the ScrollView has the allowsHitTesting(false):
struct ContentView: View {
#State private var isPresented = false
var body: some View {
ZStack(alignment: .top) {
Button("Press") {
isPresented = true
}
.frame(maxWidth: .infinity, maxHeight: 200)
.background(Color(.label).ignoresSafeArea())
ScrollView {
Spacer()
.frame(height: 200)
.allowsHitTesting(false) // <---- Will not tap thru ScrollView!!
VStack {
Button("Another Press") {
isPresented = true
}
.padding(50)
ForEach((0...50), id: \.self) {
Text("Some text \($0)")
}
}
.frame(maxWidth: .infinity)
.background(Color(.white))
}
}
.navigationBarHidden(true)
.alert(isPresented: $isPresented) {
Alert(title: Text("Button tapped"))
}
}
}
Tapping the button in the background under the Spacer doesn't work. Is there a way to apply the allowsHitTesting(false) to part of the ScrollView that the Spacer occupies? I obviously don't want to apply allowsHitTesting(false) on the entire ScrollView because this is a simplistic example but a real app would have tons of interaction views within the ScrollView. Thanks for any help or insight!

How to change the status bar background color in SwiftUI?

I have a ZStack which has Color.orange set on it:
struct HomeView: View {
init() {
UITabBar.appearance().barTintColor = UIColor.orange
}
var body: some View {
ZStack {
Color.orange
TabView {
Settings()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}
MapKitView()
.tabItem {
Image(systemName: "location.circle.fill")
Text("Home")
}
ProfileView()
.tabItem {
Image(systemName: "person")
Text("Profile")
}
}
.font(.headline)
}
}
}
and in this ZStack I have a TabView with child views that all have orange ZStacks. However these child views, including Settings() and MapKitView() shown below, do not have an orange status bar.
Settings()
struct Settings: View {
var body: some View {
ZStack {
Color.orange
VStack {
NavigationView {
...
}
}
}
}
}
MapKitView()
struct MapKitView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
return mapView
}
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapKitView>) {
}
}
How can I make the status bar orange across all my views?
If, for example, you wanted to draw a nice blur under the status bar.
YourView()
.overlay(alignment: .top, content: {
Color.clear // Or any view or color
.background(.regularMaterial) // I put clear here because I prefer to put a blur in this case. This modifier and the material it contains are optional.
.edgesIgnoringSafeArea(.top)
.frame(height: 0) // This will constrain the overlay to only go above the top safe area and not under.
})
ZStack {
...
}
.edgesIgnoringSafeArea(.vertical) // or .top

SwiftUI Bring Down Navigation Items

In my SwiftUI app, I would like to bring the navigation bar items down like in Apple's own UIKit apps.
Seen below is a screenshot from the Health app. Notice how the profile picture is in line with the 'Summary' text. This is what I am looking to achieve.
I have tried using .padding(.top, 90) but this has not worked as it does not bring down the virtual box that allows the button to be clicked. Using padding means that you have to tap the button above the image/text.
Thank you.
Unfortunately I didn't find any solution for changing navigation bar height in iOS 13 with SwiftUI, and had the same issues earlier. Solution below will fit you, if your navigation bar is always only black and you're ok with gap on the top:
struct NavBarCustomItems: View {
init() {
setNavigationBarToBlackOnly()
}
func setNavigationBarToBlackOnly() {
let blackAppearance = UINavigationBarAppearance()
blackAppearance.configureWithOpaqueBackground()
blackAppearance.backgroundColor = .black
blackAppearance.shadowColor = .clear // to avoid border line
UINavigationBar.appearance().standardAppearance = blackAppearance
UINavigationBar.appearance().scrollEdgeAppearance = blackAppearance
}
var body: some View {
NavigationView {
VStack {
NavigationBarMimicry()
// here is your content
HStack {
Text("Favorites")
Spacer()
Button(action: {}) { Text("Edit") }
}
.padding()
Spacer()
VStack {
Text("Main screen")
}
// you need spacer(s) to be sure, that NavigationBarMimicry is always on the top
Spacer()
}
}
}
}
// MARK: here is what you need in navigation bar
struct NavigationBarMimicry: View {
var body: some View {
HStack {
Text("Summary")
.bold()
.font(.system(size: 40))
.foregroundColor(.white)
.padding(.horizontal)
Spacer()
Rectangle()
.foregroundColor(.white)
.frame(width: 40)
.padding(.horizontal)
}
.background(Color.black)
.frame(height: 40)
.navigationBarTitle("", displayMode: .inline)
// you can add it to hide navigation bar, navigation will work via NavigationLink
// .navigationBarHidden(true)
}
}
struct NavBarCustomItems_Previews: PreviewProvider {
static var previews: some View {
NavBarCustomItems().environment(\.colorScheme, .dark)
}
}
the result should be like this:
P.S. maybe the other ways are:
Put views in this order: VStack { NavigationBarMimicry(); NavigationView {...}};
uncomment line of code: .navigationBarHidden(true);

Transition animation not working in SwiftUI

I'm trying to create a really simple transition animation that shows/hides a message in the center of the screen by tapping on a button:
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
}
}
}
}
According to the documentation of the .transition(.opacity) animation
A transition from transparent to opaque on insertion, and from opaque
to transparent on removal.
the message should fade in when the showMessage state property becomes true and fade out when it becomes false. This is not true in my case. The message shows up with a fade animation, but it hides with no animation at all. Any ideas?
EDIT: See the result in the gif below taken from the simulator.
The problem is that when views come and go in a ZStack, their "zIndex" doesn't stay the same. What is happening is that the when "showMessage" goes from true to false, the VStack with the "Hello World" text is put at the bottom of the stack and the yellow color is immediately drawn over top of it. It is actually fading out but it's doing so behind the yellow color so you can't see it.
To fix it you need to explicitly specify the "zIndex" for each view in the stack so they always stay the same - like so:
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow.zIndex(0)
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}.zIndex(1)
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
.zIndex(2)
}
}
}
}
My findings are that opacity transitions don't always work. (yet a slide in combination with an .animation will work..)
.transition(.opacity) //does not always work
If I write it as a custom animation it does work:
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2)))
.zIndex(1)
I found a bug in swiftUI_preview for animations. when you use a transition animation in code and want to see that in SwiftUI_preview it will not show animations or just show when some views disappear with animation. for solving this problem you just need to add your view in preview in a VStack. like this :
struct test_UI: View {
#State var isShowSideBar = false
var body: some View {
ZStack {
Button("ShowMenu") {
withAnimation {
isShowSideBar.toggle()
}
}
if isShowSideBar {
SideBarView()
.transition(.slide)
}
}
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
VStack {
SomeView()
}
}
}
after this, all animations will happen.
I believe this is a problem with the canvas. I was playing around with transitions this morning and while the don't work on the canvas, they DO seem to work in the simulator. Give that a try. I've reported the bug to Apple.
I like Scott Gribben's answer better (see below), but since I cannot delete this one (due to the green check), I'll just leave the original answer untouched. I would argue though, that I do consider it a bug. One would expect the zIndex to be implicitly assigned by the order views appear in code.
To work around it, you may embed the if statement inside a VStack.
struct ContentView: View {
#State private var showMessage = false
var body: some View {
ZStack {
Color.yellow
VStack {
Spacer()
Button(action: {
withAnimation(.easeOut(duration: 3)) {
self.showMessage.toggle()
}
}) {
Text("SHOW MESSAGE")
}
}
VStack {
if showMessage {
Text("HELLO WORLD!")
.transition(.opacity)
}
}
}
}
}
zIndex may cause the animation to be broken when interrupted. Wrap the view you wanna apply transition to in a VStack, HStack or any other container will make sense.
I just gave up on .transition. It's just not working. I instead animated the view's offset, much more reliable:
First I create a state variable for offset:
#State private var offset: CGFloat = 200
Second, I set the VStack's offset to it. Then, in its .onAppear(), I change the offset back to 0 with animation:
VStack{
Spacer()
HStack{
Spacer()
Image("MyImage")
}
}
.offset(x: offset)
.onAppear {
withAnimation(.easeOut(duration: 2.5)) {
offset = 0
}
}
Below code should work.
import SwiftUI
struct SwiftUITest: View {
#State private var isAnimated:Bool = false
var body: some View {
ZStack(alignment:.bottom) {
VStack{
Spacer()
Button("Slide View"){
withAnimation(.easeInOut) {
isAnimated.toggle()
}
}
Spacer()
Spacer()
}
if isAnimated {
RoundedRectangle(cornerRadius: 16).frame(height: UIScreen.main.bounds.height/2)
.transition(.slide)
}
}.ignoresSafeArea()
}
}
struct SwiftUITest_Previews: PreviewProvider {
static var previews: some View {
VStack {
SwiftUITest()
}
}
}

Resources