I have a CameraView in my app that I'd like to bring up whenever a button is to be presssed. It's a custom view that looks like this
// The CameraView
struct Camera: View {
#StateObject var model = CameraViewModel()
#State var currentZoomFactor: CGFloat = 1.0
#Binding var showCameraView: Bool
// MARK: [main body starts here]
var body: some View {
GeometryReader { reader in
ZStack {
// This black background lies behind everything.
Color.black.edgesIgnoringSafeArea(.all)
CameraViewfinder(session: model.session)
.onAppear {
model.configure()
}
.alert(isPresented: $model.showAlertError, content: {
Alert(title: Text(model.alertError.title), message: Text(model.alertError.message), dismissButton: .default(Text(model.alertError.primaryButtonTitle), action: {
model.alertError.primaryAction?()
}))
})
.scaledToFill()
.ignoresSafeArea()
.frame(width: reader.size.width,height: reader.size.height )
// Buttons and controls on top of the CameraViewfinder
VStack {
HStack {
Button {
//
} label: {
Image(systemName: "xmark")
.resizable()
.frame(width: 20, height: 20)
.tint(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
Spacer()
flashButton
}
HStack {
capturedPhotoThumbnail
Spacer()
captureButton
Spacer()
flipCameraButton
}
.padding([.horizontal, .bottom], 20)
.frame(maxHeight: .infinity, alignment: .bottom)
}
} // [ZStack Ends Here]
} // [Geometry Reader Ends here]
} // [Main Body Ends here]
// More view component code goes here but I've excluded it all for brevity (they don't add anything substantial to the question being asked.
} // [End of CameraView]
It contains a CameraViewfinder View which conforms to the UIViewRepresentable Protocol:
struct CameraViewfinder: UIViewRepresentable {
class VideoPreviewView: UIView {
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
let session: AVCaptureSession
func makeUIView(context: Context) -> VideoPreviewView {
let view = VideoPreviewView()
view.backgroundColor = .black
view.videoPreviewLayer.cornerRadius = 0
view.videoPreviewLayer.session = session
view.videoPreviewLayer.connection?.videoOrientation = .portrait
return view
}
func updateUIView(_ uiView: VideoPreviewView, context: Context) {
}
}
I wish to add a binding property to this camera view that allows me to toggle this view in and out of my screen like any other social media app would allow. Here's an example
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView(showCamera: $showCamera)
}
}
I understand that the code to achieve this must be written inside the updateUIView() method. Now, although I'm quite familiar with SwiftUI, I'm relatively inexperienced with UIKit, so any help on this and any helpful resources that could help me better code situations similar to this would be greatly appreciated.
Thank you.
EDIT: Made it clear that the first block of code is my CameraView.
EDIT2: Added Example of how I'd like to use the CameraView in my App.
Judging by the way you would like to use it in the app, the issue seems to not be with the CameraViewFinder but rather with the way in which you want to present it.
A proper SwiftUI way to achieve this would be to use a sheet like this:
#State var showCamera: Bool = false
var body: some View {
mainTabView
.sheet(isPresented: $showCamera) {
CameraView()
.interactiveDismissDisabled() // Disables swipe to dismiss
}
}
If you don't want to use the sheet presentation and would like to cover the whole screen instead, then you should use the .fullScreenCover() modifier like this.
#State var showCamera: Bool = false
var body: some View {
mainTabView
.overlay {
CameraView()
.fullScreenCover(isPresented: $showCamera)
}
}
Either way you would need to somehow pass the state to your CameraView to allow the presented screen to set the state to false and therefore dismiss itself, e.g. with a button press.
I just started coding in SwiftUI and came across a problem. I need to give different colors to the background of the navigation bar (NavigationView). The colors will change as I go from one view to the next. I need to have this working for navigationBarTitleDisplayMode being "inline".
I tried the solutions presented in:
SwiftUI update navigation bar title color
but none of these solutions work fully for what I need.
The solution in this reply to that post works for inline:
Using UIViewControllerRepresentable. Nevertheless, when we first open the view it will show the color of the previous view for one second, before changing to the new color. I would like to avoid this and have the color displayed as soon as everything appears on screen. Is there a way to do this?
This other solution will not work either: Changing UINavigation's appearance in init(), because when I set the background in init(), it will change the background of all the views in the app. Again, I need the views to have different background colors.
I tried something similar to this solution: Modifying Toolbar, but it does not allow me to change the color of the navigation bar.
The other solution I tried was this: Creating navigationBarColor function, which is based on: NAVIGATIONVIEW DYNAMIC BACKGROUND COLOR IN SWIFTUI. This solution works for navigationBarTitleDisplayMode "large", but when setting navigationBarTitleDisplayMode to "inline", it will show the background color of the navigation bar in a different color, as if it was covered by a gray/transparent layer. For example, the color it shows in "large" mode is:
Red color in large mode
But instead, it shows this color:
Red color in inline mode
Finally, I tried this solution: Subclassing UIViewController and configuring viewDidLayoutSubviews(), but it did not work for what I want it either.
The closest solutions for what I need are 1. and 4., but they still do not work 100%.
Would anybody know how to make any of these solutions work for navigationBarTitleDisplayMode inline, being able to change the background color of the navigation bar in different layouts, and showing the new color once the view is shown (without delays)?
Thank you!
By the way, I am using XCode 12.5.
Here is the sample code that I am using, taking example 4. as a model:
FirstView.swift
import SwiftUI
struct FirstView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
GeometryReader { metrics in
VStack {
Text("This is the first view")
NavigationLink(destination: SecondView(), tag: "SecondView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "SecondView"
print("Go to second view")
}) {
Text("Go to second view")
}
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
On this screen, if I use
.navigationBarTitleDisplayMode(.large)
the color will be displayed properly: Navigation bar with red color
But using
.navigationBarTitleDisplayMode(.inline)
there is a blur on it: Navigation bar with some sort of blur over red color
import SwiftUI
struct SecondView: View {
#State private var selection: String? = nil
var body: some View {
GeometryReader { metrics in
VStack {
Text("This is the second view")
NavigationLink(destination: ThirdView(), tag: "ThirdView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "ThirdView"
print("Go to third view")
}) {
Text("Go to third view")
}
}
}
.navigationBarColor(backgroundColor: Color.red, titleColor: .black)
.navigationBarTitleDisplayMode(.inline)
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
ThirdView.swift
This view displays the color properly as it is using
.navigationBarTitleDisplayMode(.large)
But if changed to
.navigationBarTitleDisplayMode(.inline)
it will show the blur on top of the color as well.
import SwiftUI
struct ThirdView: View {
var body: some View {
GeometryReader { metrics in
Text("This is the third view")
}
.navigationBarColor(backgroundColor: Color.blue, titleColor: .black)
.navigationBarTitleDisplayMode(.large)
}
}
struct ThirdView_Previews: PreviewProvider {
static var previews: some View {
ThirdView()
}
}
NavigationBarModifierView.swift
import SwiftUI
struct NavigationBarModifier: ViewModifier {
var backgroundColor: UIColor?
var titleColor: UIColor?
init(backgroundColor: Color, titleColor: UIColor?) {
self.backgroundColor = UIColor(backgroundColor)
let coloredAppearance = UINavigationBarAppearance()
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = UIColor(backgroundColor)
coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().compactAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
UINavigationBar.appearance().tintColor = titleColor
}
func body(content: Content) -> some View {
ZStack{
content
VStack {
GeometryReader { geometry in
Color(self.backgroundColor ?? .clear)
.frame(height: geometry.safeAreaInsets.top)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
}
extension View {
func navigationBarColor(backgroundColor: Color, titleColor: UIColor?) -> some View {
self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
}
}
NOTE TO THE MODERATORS: Please, do not delete this post. I know similar questions were asked before, but I need an answer to this in particular which was not addressed. Please read before deleting indiscriminately, I need this for work. Also, I cannot ask questions inline in each of those solutions because I do not have the minimum 50 points in stackoverflow required to write there.
I think I have what you want. It is VERY touchy... It is a hack, and not terribly robust, so take as is...
I got it to work by having your modifier return a clear NavBar, and then the solution from this answer works for you. I even added a ScrollView to ThirdView() to make sure that scrolling under didn't affect in. Also note, you lose all of the other built in effects of the bar like translucency, etc.
Edit: I went over the code. The .navigationViewStyle was in the wrong spot. It likes to be outside of the NavigaionView(), where everything else needs to be inside. Also, I removed the part of the code setting the bar color in FirstView() as it was redundant and ugly. I hadn't meant to leave that in there.
struct NavigationBarModifier: ViewModifier {
var backgroundColor: UIColor?
var titleColor: UIColor?
init(backgroundColor: Color, titleColor: UIColor?) {
self.backgroundColor = UIColor(backgroundColor)
let coloredAppearance = UINavigationBarAppearance()
coloredAppearance.configureWithTransparentBackground()
coloredAppearance.backgroundColor = .clear // The key is here. Change the actual bar to clear.
coloredAppearance.titleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.largeTitleTextAttributes = [.foregroundColor: titleColor ?? .white]
coloredAppearance.shadowColor = .clear
UINavigationBar.appearance().standardAppearance = coloredAppearance
UINavigationBar.appearance().compactAppearance = coloredAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
UINavigationBar.appearance().tintColor = titleColor
}
func body(content: Content) -> some View {
ZStack{
content
VStack {
GeometryReader { geometry in
Color(self.backgroundColor ?? .clear)
.frame(height: geometry.safeAreaInsets.top)
.edgesIgnoringSafeArea(.top)
Spacer()
}
}
}
}
}
extension View {
func navigationBarColor(backgroundColor: Color, titleColor: UIColor?) -> some View {
self.modifier(NavigationBarModifier(backgroundColor: backgroundColor, titleColor: titleColor))
}
}
struct FirstView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
GeometryReader { _ in
VStack {
Text("This is the first view")
NavigationLink(destination: SecondView(), tag: "SecondView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "SecondView"
print("Go to second view")
}) {
Text("Go to second view")
}
}
.navigationTitle("First")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .red, titleColor: .black)
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SecondView: View {
#State private var selection: String? = nil
var body: some View {
VStack {
Text("This is the second view")
NavigationLink(destination: ThirdView(), tag: "ThirdView", selection: $selection) {
EmptyView()
}
Button(action: {
self.selection = "ThirdView"
print("Go to third view")
}) {
Text("Go to third view")
}
}
.navigationTitle("Second")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .blue, titleColor: .black)
}
}
struct ThirdView: View {
var body: some View {
ScrollView {
ForEach(0..<50) { _ in
Text("This is the third view")
}
}
.navigationTitle("Third")
.navigationBarTitleDisplayMode(.inline)
.navigationBarColor(backgroundColor: .green, titleColor: .black)
}
}
iOS 16
Since this version of SwiftUI, there is a dedicated modifier for setting any toolbar background color (including the navigation bar):
Xcode 14 beta 5 (Not working 🤦🏻♂️, waiting for beta 6...)
.toolbarBackground(.red, for: .navigationBar)
Xcode 14 beta 1,2,3,4
.toolbarBackground(.red, in: .navigationBar)
It works perfectly in in inline mode and also animates between modes.
For my custom view the following code worked well.
struct HomeView: View {
init() {
//Use this if NavigationBarTitle is with Large Font
UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.systemIndigo]
//Use this if NavigationBarTitle is with displayMode = .inline
UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.systemIndigo]
UINavigationBar.appearance().backgroundColor = UIColor.clear
UINavigationBar.appearance().barTintColor = UIColor(Color(red: 32 / 255, green: 72 / 255, blue: 63 / 255))
}
var body: some View {
NavigationView {
ZStack {
...
...
...
}
.padding(.zero)
.navigationTitle("Feedbacks")
}
}
}
and result is like that:
Here is a bit hacky solution, but it works for me (as of iOS 15) both for .large and .inline display modes.
import SwiftUI
enum Kind: String, CaseIterable {
case checking
case savings
case investment
}
struct PaddedList: View {
#Binding var name: String
#Binding var kind: Kind
var body: some View {
NavigationView {
List {
TextField("Account name", text: $name)
Picker("Kind", selection: $kind) {
ForEach(Kind.allCases, id: \.self) { kind in
Text(kind.rawValue).tag(kind)
}
}
.listRowSeparatorTint(.red)
Spacer()
}
.padding(.top, 1) // note top 1 padding!
.background(.green) // the color "bleeds" through
.navigationBarTitle("Navigation Bar")
}
}
}
struct PaddedList_Previews: PreviewProvider {
static var previews: some View {
PaddedList(name: .constant(""), kind: .constant(.checking))
}
}
It looks like Navigation + TabView + Sheet is broken in iOS 15.
When I do this:
ContentView -> DetailView -> Bottom Sheet
When the bottom sheet comes up, the Detail view is automatically popped off the stack:
https://www.youtube.com/watch?v=gguLptAx0l4
I expect the Detail view to stay there even when the bottom sheet appears. Does anyone have any idea on why this happens and how to fix it?
Here is my sample code:
import Combine
import SwiftUI
import RealmSwift
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
TabItemView(num: 1)
.tabItem {
Text("One")
}
TabItemView(num: 2)
.tabItem {
Text("Two")
}
}
}
}
}
struct TabItemView: View {
private let num: Int
init(num: Int) {
self.num = num
}
var body: some View {
NavigationLink(destination: DetailView(text: "Detail View \(num)")) {
Text("Go to Detail View")
}
}
}
struct DetailView: View {
#State private var showingSheet = false
private let text: String
init(text: String) {
self.text = text
}
var body: some View {
Button("Open Sheet") {
showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
Text("Sheet Text")
}
}
}
This works on iOS 14 btw
UPDATE 1:
Tried #Sebastian's suggestion of putting NavigationView inside of TabView. While this fixed the nav bug, it fundamentally changed the behavior (I don't want to show the tabs in DetailView).
Also tried his suggestion of using Introspect to set navigationController.hidesBottomBarWhenPushed = true on the NavigationLink destination, but that didn't do anything:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TabItemView(num: 1)
}.tabItem {
Text("One")
}
NavigationView {
TabItemView(num: 2)
}.tabItem {
Text("Two")
}
}
}
}
struct TabItemView: View {
private let num: Int
init(num: Int) {
self.num = num
}
var body: some View {
NavigationLink(destination: DetailView(text: "Detail View \(num)").introspectNavigationController { navigationController in
navigationController.hidesBottomBarWhenPushed = true
}) {
Text("Go to Detail View")
}
}
}
struct DetailView: View {
#State private var showingSheet = false
private let text: String
init(text: String) {
self.text = text
}
var body: some View {
Button("Open Sheet") {
showingSheet.toggle()
}.sheet(isPresented: $showingSheet) {
Text("Sheet Text")
}
}
}
You need to flip how you nest TabView & NavigationView. Instead of nesting several TabView views inside a NavigationView, use the TabView as the parent component, with a NavigationView for each tab.
This is how the updated ContentView would look like:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TabItemView(num: 1)
}
.tabItem {
Text("One")
}
NavigationView {
TabItemView(num: 2)
}
.tabItem {
Text("Two")
}
}
}
}
This makes sense and is more correct: The tabs should always be visible, but you want to show a different navigation stack with different content in each tab.
That it worked previously doesn't make it more correct - SwiftUI probably just changed its mind on dealing with unexpected situations. That, and the lack of error messages in these situations, is the downside of using a framework that tries to render anything you throw at it!
If the goal is specifically to hide the tabs when pushing a new view on a NavigationView (e.g., when tapping on a conversation in a messaging app), you have to use a different solution. Apple added the UIViewController.hidesBottomBarWhenPushed property to UIKit to support this specific use case.
This property is set on the UIViewController that, when presented, should not show a toolbar. In other words: Not the UINavigationController or the UITabBarController, but the child UIViewController that you push onto the UINavigationController.
This property is not supported in SwiftUI natively. You could set it using SwiftUI-Introspect, or simply write the navigation structure of your application using UIKit and write the views inside in SwiftUI, linking them using UIHostingViewController.
Code I used to create an example of this navigation bar:
struct ContentView: View {
init() {
let defaultAppearance = UINavigationBarAppearance()
defaultAppearance.configureWithOpaqueBackground()
defaultAppearance.backgroundColor = .red
UINavigationBar.appearance().standardAppearance = defaultAppearance
UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
}
var body: some View {
ParentView()
}
}
struct ParentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Tap here")
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.all)
}
}
}
struct DetailView: View {
var body: some View {
VStack {
Text("Detail View")
}
.edgesIgnoringSafeArea(.all)
}
}
It doesn't matter where I'm putting .edgesIgnoringSafeArea(.all) it doesn't work. Is there another way to tell Navigation Bar to ignore the safe area?
If it's not the safe area problem, then I need to make the whole navigation bar to have the same height of UINavigationBarContentView displayed in UI Hierarchy:
I've got a very simple VStack, based directly off of one of Paul Hudson's excellent SwiftUI samples. There are two lines of Text, one hidden. There's a method to toggle an #State var which controls the hidden Text.
If I call that function from within the VStack, it animates properly. If I call it from a navigationBarItems, it loses the animation. Am I missing something about how views are composed?
struct ContentView: View {
#State var showDetails = false
func toggleDetails() { withAnimation { self.showDetails.toggle() } }
var body: some View {
NavigationView() {
VStack {
Button(action: { self.toggleDetails() }) { Text("Tap to show details") }
if showDetails { Text("Details go here.") }
}
.navigationBarTitle(Text("Nav Bar"))
.navigationBarItems(trailing:
Button(action: { self.toggleDetails() }) {
Text("Details")
})
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Beta 5 Update
It seems beta 5 fixed this problem. Workaround no longer needed.
Workaround for beta 4 and previous versions
I think the reason it does not work, is because you are calling withAnimation from a different branch of the view tree. The "Details" button and the views that need to be animated are on different branches of the hierarchy. I am just guessing, but it seems to be supported by the workaround I posted here.
If instead of using explicit animations (i.e., withAnimation), you use implicit animations on both the VStack and the Text, it works:
struct ContentView: View {
#State var showDetails = false
func toggleDetails() { self.showDetails.toggle() }
var body: some View {
NavigationView() {
VStack {
Button(action: { self.toggleDetails() }) { Text("Tap to show details") }
if showDetails {
Text("Details go here.").animation(.basic())
}
}
.animation(.basic())
.navigationBarTitle(Text("Nav Bar"))
.navigationBarItems(trailing:
Button(action: {
self.toggleDetails()
}) { Text("Details") })
}
}
}