SwiftUI & Mac Catalyst | Adding buttons to trailing navigation bar - ios

I have a Mac Catalyst app I had built using SwiftUI,and I cant seem to add buttons to the trailing navigation bar?
I am also unsure where this navigationBar is defined, is it possible to remove? It only seems to have appeared in Ventura.
struct AppSidebarNavigation: View {
enum NavigationItem {
case home
}
#State private var selection: NavigationItem? = .home
init() {
#if !targetEnvironment(macCatalyst)
UITableView.appearance().backgroundColor = UIColor(named: "White")
UITableViewCell.appearance().selectionStyle = .none
UITableView.appearance().allowsSelection = false
#endif
}
var body: some View {
NavigationView {
sidebar
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
// Main View
HomeView()
.navigationTitle("")
.edgesIgnoringSafeArea(.top)
.navigationBarHidden(isMac ? false : true)
.navigationBarBackButtonHidden(isMac ? false : true)
}
.accentColor(Color("Black"))
.navigationViewStyle(.columns)
}
}
HomeView I had the following to the View.
#if targetEnvironment(macCatalyst)
.navigationBarItems(trailing:
NavButtons
)
#endif
var NavButtons: some View {
HStack {
Button(action: {
Print("No")
}) {
Image(systemName: "plus")
.font(.system(size: 14, weight: .medium))
}
.buttonStyle(NavPlusButton())
}
}

I don't think it is possible to do that because:
#available(iOS, introduced: 13.0, deprecated: 100000.0, message: "Use toolbar(_:) with navigationBarLeading or navigationBarTrailing placement")
#available(tvOS, introduced: 13.0, deprecated: 100000.0, message: "Use toolbar(_:) with navigationBarLeading or navigationBarTrailing placement")
#available(macOS, unavailable)
#available(watchOS, unavailable)
public func navigationBarItems<T>(trailing: T) -> some View where T : View
This indicates on macOS the function in not available.
I slightly modified your code (to get it to compile) and saw that where it would be is at the red circle below:
My code is:
struct AppSidebarNavigation: View {
enum NavigationItem {
case home
}
#State private var selection: NavigationItem? = .home
var isMac: Bool
init() {
#if !targetEnvironment(macCatalyst)
UITableView.appearance().backgroundColor = UIColor(named: "White")
UITableViewCell.appearance().selectionStyle = .none
UITableView.appearance().allowsSelection = false
isMac = false
#else
isMac = true
#endif
}
var body: some View {
NavigationView {
// Main View
HomeView()
.navigationTitle("NavTitle")
.edgesIgnoringSafeArea(.top)
.navigationBarHidden(isMac ? false : true)
.navigationBarBackButtonHidden(isMac ? false : true)
.navigationBarItems(trailing: NavButtons)
}
.accentColor(Color("Black"))
.navigationViewStyle(.columns)
}
}
var NavButtons: some View {
HStack {
Button(action: {}) {
Image(systemName: "plus")
}
// Button(action: {
// print("No")
// }) {
// Image(systemName: "plus")
// .font(.system(size: 14, weight: .medium)).frame(width: 80)
// }
}
}
struct HomeView: View {
var body: some View {
Group {
Text("Home View")
NavButtons
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
struct NavButton_Previews: PreviewProvider {
static var previews: some View {
NavButtons
}
}
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
AppSidebarNavigation()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

You can use Toolbar modifier with .primaryAction as placement to place button on the trailing side on Navigation Bar.
Just replace .navigationBarItem with below:
#if targetEnvironment(macCatalyst)
.toolbar {
ToolbarItem(placement: .primaryAction) {
NavButtons
}
}
#endif

Related

How to prevent keyboard dismiss when bottomsheet is dismiss using swipe gesture (Interactive dismiss) iOS

I have a requirement where we need to keep close active unless bottom sheet is not closed. I have seen similar implementations in iOS maps where search keyboard is opened until the view is fully dismissed.
What I have currently :-
What I want :- https://drive.google.com/file/d/1SmXniFp0ZTF5igMzk6gk3eflCeP1xjn6/view?usp=share_link
This is code which i use to to present iOS native sheet :-
struct ContentView: View {
#State var isPresented = false
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.onTapGesture {
isPresented.toggle()
}
.sheet(isPresented: $isPresented) {
BottomSheetViewRepresentable(content: {
NavigationView {
DemoView()
}
.navigationTitle("Hey")
}, detents: [.large()])
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DemoView: View {
var body: some View{
if #available(iOS 16.0, *) {
ZStack {
TextField("", text: .constant("Hey"))
}
.scrollDismissesKeyboard(.never)
} else {
// Fallback on earlier versions
Color.yellow
}
}
}
I see you have added .scrollDismissesKeyboard(.never) on the ZStack which seems not working.
Try wrapping content in a List or ScrollView, then set scrollDismissesKeyboard to never.
Examples -
var body: some View {
ScrollView {
VStack {
TextField("Name", text: $username)
.textFieldStyle(.roundedBorder)
TextEditor(text: $bio)
.frame(height: 400)
.border(.quaternary, width: 1)
}
.padding(.horizontal)
}
.scrollDismissesKeyboard(.never)
}
List example -
struct KeyboardDismissExample: View {
#State private var text = ""
var body: some View {
List {
ForEach(0..<100) { i in
TextField(("Item \(i)"), text: .constant(""))
}
}
.scrollDismissesKeyboard(.never)
}
}

SwiftUI EditButton() does not work when set editMode in NavigationLink

I have EditNoteHost view which displays NoteView or EditNote depending on editMode environment variable. Aslo Cancel button is displayed in edit mode:
struct EditNoteHost: View {
#EnvironmentObject var modelData: ModelData
#Environment(\.editMode) var editMode
#State private var draftNote = Note.default
var body: some View {
VStack(alignment: .leading, spacing: 20) {
HStack {
if editMode?.wrappedValue == .active {
Button("Cancel") {
draftNote = modelData.selectedNote
editMode?.animation().wrappedValue = .inactive
}
}
Spacer()
EditButton()
}
if editMode?.wrappedValue == .inactive {
NoteView(note: modelData.selectedNote)
} else {
EditNote(note: $draftNote)
.onAppear {
draftNote = modelData.selectedNote
}
.onDisappear {
modelData.selectedNote = draftNote
}
}
}
.padding()
}
}
struct EditNoteHost_Previews: PreviewProvider {
static var previews: some View {
EditNoteHost()
.environmentObject(ModelData())
}
}
This code works fine.
Now I want to use EditNoteHost in NavigationLink and start it in edit mode:
NavigationLink(destination: EditNoteHost().environment(\.editMode, Binding.constant(EditMode.active)).environmentObject(modelData)) {
Image(systemName: "plus")
}
This part of code opens EditNoteHost in edit mode if click +. However, Done and Cancel buttons do nothing when tap.
How can I fix this?
Screenshot
You are setting editMode using .constant(EditMode.active), it will remain active. So do not set the environment for editMode; instead use:
NavigationLink(destination: EditNoteHost()) {
Image(systemName: "plus")
}
and in EditNoteHost, use:
.onAppear() {
editMode?.animation().wrappedValue = .active
}

View not updating during a transition

I need your help to understand an issue I'm having. I can't manage to make a view redraws its body just before a transition animation. Take a look at this simple example:
import SwiftUI
struct ContentView: View {
#State private var condition = true
#State private var fgColor = Color.black
var body: some View {
VStack {
Group {
if condition {
Text("Hello")
.foregroundColor(fgColor)
} else {
Text("World")
}
}
.transition(.slide)
.animation(.easeOut(duration: 3))
Button("TAP") {
fgColor = .red
condition.toggle()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
What I expected from this example was: when I tap on the button the view creates its body again and the text "Hello" becomes red. Now, the view creates its body another time and the transition happens. Instead, it seems that SwiftUI merges the two state changes somehow and only the second one is considered. The result is that the transition happens, but the text "Hello" won't change its color.
How can I manage a situation like this in SwiftUI? Is there a way to tell the framework to update the two state changes separately? Thank you.
EDIT for #Asperi:
I tried your code but it doesn't work. The result is still the same. This is the complete example with your code:
import SwiftUI
struct ContentView: View {
#State private var condition = true
#State private var fgColor = Color.black
var body: some View {
VStack {
VStack {
if condition {
Text("Hello")
.foregroundColor(fgColor)
.transition(.slide)
} else {
Text("World")
.transition(.slide)
}
}
.animation(.easeOut(duration: 3))
Button("TAP") {
fgColor = .red
condition.toggle()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And this is the result (on iPhone 12 Mini iOS 14.1):
It's impossible to do what you want with just modifiers.
Because when change the view state, the code block with Text("Hello") doesn't get's called, and that's the reason it starts disappearing with transition you've specifier(because this view is missing in the view hierarchy)
So the best way you can do it is implementing your custom transition. At first you need to create a ViewModifier with your desired behavior:
struct ForegroundColorModifier: ViewModifier {
var foregroundColor: Color
func body(content: Content) -> some View {
content
// for some reason foregroundColor for text is not animatable by itself, but can animate with colorMultiply
.foregroundColor(.white)
.colorMultiply(foregroundColor)
}
}
Then create a Transition using this modifier - here you need to specify modifiers for two states:
extension AnyTransition {
static func foregroundColor(active: Color, identity: Color) -> AnyTransition {
AnyTransition.modifier(
active: ForegroundColorModifier(foregroundColor: active),
identity: ForegroundColorModifier(foregroundColor: identity)
)
}
}
Combine color transition with a slide one:
.transition(
AnyTransition.slide.combined(
with: .foregroundColor(
active: .red,
identity: .black
)
)
)
Finally If you need color change only on disappearing, use asymmetric transition:
.transition(
.asymmetric(
insertion: .slide,
removal: AnyTransition.slide.combined(
with: .foregroundColor(
active: .red,
identity: .black
)
)
)
)
Full code:
struct ContentView: View {
#State private var condition = true
var body: some View {
VStack {
Group {
if condition {
Text("Hello")
} else {
Text("World")
}
}
.transition(
.asymmetric(
insertion: .slide,
removal: AnyTransition.slide.combined(
with: .foregroundColor(
active: .red,
identity: .black
)
)
)
)
Button("TAP") {
withAnimation(.easeOut(duration: 3)) {
condition.toggle()
}
}
}
}
}
I'm not sure why specifying .animation after .transition doesn't work in this case, but changing state inside withAnimation block works as expected
Here is a possible solution:
struct ContentView: View {
#State private var condition = true
#State private var fgColor = Color.black
var body: some View {
VStack {
Group {
if condition {
Text("Hello")
.foregroundColor(fgColor)
} else {
Text("World")
}
}
.transition(.slide)
.animation(.easeOut(duration: 3))
Button("TAP") {
fgColor = .red
withAnimation {
condition.toggle()
}
}
}
}
}
The idea here is that you propagate the transition that was caused by condition change by changing it inside withAnimation (which is delayed compared to the foreground color change with no animation).
The main issue is in using Group - it is not a container, instead use some real container and apply transitions to views directly, like
var body: some View {
VStack {
VStack {
if condition {
Text("Hello")
.foregroundColor(fgColor)
.transition(.slide)
} else {
Text("World")
.transition(.slide)
}
}
.animation(.easeOut(duration: 3))
Button("TAP") {
fgColor = .red
condition.toggle()
}
}
}
The most easier and logical way of making your code work, updating View before Animation take the control.
Version 1.0.0:
import SwiftUI
struct ContentView: View {
#State private var condition = true
#State private var fgColor = Color.black
var body: some View {
VStack(spacing: 40.0) {
Group {
if condition {
Text("Hello")
.foregroundColor(fgColor)
}
else {
Text("World")
}
}
.transition(.slide)
.animation(.easeOut(duration: 3))
Button("TAP") {
fgColor = .red
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(50)) { condition.toggle() }
}
}
}
}
Version 2.0.0:
import SwiftUI
struct ContentView: View {
#State private var condition: Bool = true
#State private var fgColor: Color = Color.green
#State private var startTransition: Bool = Bool()
var body: some View {
VStack(spacing: 40.0) {
Group {
if condition {
Text("Hello")
.foregroundColor(fgColor)
}
else {
Text("World")
}
}
.transition(.slide)
.animation(.easeOut(duration: 3))
.onChange(of: startTransition) { _ in condition.toggle() }
Button("TAP") {
if fgColor == Color.red { fgColor = Color.green } else { fgColor = Color.red }
startTransition.toggle()
}
}
}
}
Try this,
struct ContentView: View {
#State private var condition = true
#State private var fgColor = Color.black
var body: some View {
VStack {
HStack {
Text("Hello")
.foregroundColor(fgColor)
if !condition {
Text("World")
}
}
.transition(.slide)
.animation(.easeOut(duration: 3))
Button("TAP") {
fgColor = .red
condition.toggle()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How can I hide TabBar Swift UI?

I have a TabBarView in the root view. In one of the parent views that's nested within the root view, I'd like the tab bar to hide when navigating from that parent view to the child view. Is there any func or command to handle that?
Something like this:
ContentView (with TabBarView) - > ExploreView (Called in TabBarView ) -> MessagesView (Child of ExploreVIew - Hide Tab bar)
My code can be seen below.
TabView{
NavigationView{
GeometryReader { geometry in
ExploreView()
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading: Button(action: {
}, label: {
HStack{
Image("cityOption")
Text("BER")
Image("arrowCities")
}.foregroundColor(Color("blackAndWhite"))
.font(.system(size: 12, weight: .semibold))
}),trailing:
HStack{
Image("closttop")
.renderingMode(.template)
.padding(.trailing, 125)
NavigationLink(destination: MessagesView()
.navigationBarTitle(Text("Messages"), displayMode: .inline)
.navigationBarItems(trailing: Image("writemessage"))
.foregroundColor(Color("blackAndWhite"))
){
Image("messages")
}
}.foregroundColor(Color("blackAndWhite"))
)
}
}
.tabItem{
HStack{
Image("clostNav").renderingMode(.template)
Text("Explore")
.font(.system(size: 16, weight: .semibold))
}.foregroundColor(Color("blackAndWhite"))
}
SearchView().tabItem{
Image("search").renderingMode(.template)
Text("Search")
}
PostView().tabItem{
HStack{
Image("post").renderingMode(.template)
.resizable().frame(width: 35, height: 35)
}
}
OrdersView().tabItem{
Image("orders").renderingMode(.template)
Text("Orders")
}
ProfileView().tabItem{
Image("profile").renderingMode(.template)
Text("Profile")
}
}
Appreciate any help! Thanks!
Create CustumPresentViewController.swift -
import UIKit
import SwiftUI
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder { return
ViewControllerHolder(value:
UIApplication.shared.windows.first?.rootViewController ) }
}
extension EnvironmentValues {
var viewController: ViewControllerHolder {
get { return self[ViewControllerKey.self] }
set { self[ViewControllerKey.self] = newValue }
}
}
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle =
.automatic, #ViewBuilder builder: () -> Content) {
// Must instantiate HostingController with some sort of view...
let toPresent = UIHostingController(rootView:
AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
// ... but then we can reset rootView to include the environment
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, ViewControllerHolder(value:
toPresent))
)
self.present(toPresent, animated: true, completion: nil)
}
}
Use this in required View -
#Environment(\.viewController) private var viewControllerHolder:
ViewControllerHolder
private var viewController: UIViewController? {
self.viewControllerHolder.value
}
var body: some View {
NavigationView {
ZStack {
Text("Navigate")
}.onTapGesture {
self.viewController?.present(style: .fullScreen) {
EditUserView()
}
}
}
}
A bit late here, on iOS 16 you could use ContentView().toolbar(
.hidden, for: .tabBar)
So in your case, it would be like as below,
struct ExploreView: View {
var body: some View {
some_view {
}
.toolbar(.hidden, for: .tabBar)
}
}
in a normal iphone environment the tabbar vanishes from itself if you are navigating....or are u running in another environment?
check this:
struct ContentView: View {
#State var hideTabbar = false
#State var navigate = false
var body: some View {
NavigationView {
TabView {
VStack {
NavigationLink(destination: Text("without tab")){
Text("aha")
}
.tabItem {
Text("b")
}
}.tag(0)
Text("Second View")
.tabItem {
GeometryReader { geometry in
Image(systemName: "2.circle")
// Text("Second")
Text("\(geometry.size.height) - \(geometry.size.width)")
}
}.tag(1)
}
}
}//.isHidden(self.hideTabbar)
}

Show leading navigationBarItems button only if shown as a modal

I have a view that can be shown either as a modal, or simply pushed onto a navigation stack. When it's pushed, it has the back button in the top left, and when it's shown as a modal, I want to add a close button (many of my testers were not easily able to figure out that they could slide down the modal and really expected an explicit close button).
Now, I have multiple problems.
How do I figure out if a View is shown modally or not? Or alternatively, if it's not the first view on a navigation stack? In UIKit there are multiple ways to easily do this. Adding a presentationMode #Environment variable doesn't help, because its isPresented value is also true for pushed screens. I could of course pass in a isModal variable myself but it seems weird that's the only way?
How do I conditionally add a leading navigationBarItem? The problem is that if you give nil, even the default back button is hidden.
Code to copy and paste into Xcode and play around with:
import SwiftUI
struct ContentView: View {
#State private var showModal = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.showModal = true
}
NavigationLink("Push", destination: DetailView(isModal: false))
}
.navigationBarTitle("Home")
}
.sheet(isPresented: $showModal) {
NavigationView {
DetailView(isModal: true)
}
}
}
}
struct DetailView: View {
#Environment(\.presentationMode) private var presentationMode
let isModal: Bool
var body: some View {
Text("Hello World")
.navigationBarTitle(Text("Detail"), displayMode: .inline)
.navigationBarItems(leading: closeButton, trailing: deleteButton)
}
private var closeButton: some View {
Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
Image(systemName: "xmark")
.frame(height: 36)
}
}
private var deleteButton: some View {
Button(action: { print("DELETE") }) {
Image(systemName: "trash")
.frame(height: 36)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If I change closeButton to return an optional AnyView? and then return nil when isModal is false, I don't get a back button at all. I also can't call navigationBarItems twice, once with a leading and once with a trailing button, because the latter call overrides the first call. I'm kinda stuck here.
Okay, I managed it. It's not pretty and I am very much open to different suggestions, but it works 😅
import SwiftUI
extension View {
func eraseToAnyView() -> AnyView {
AnyView(self)
}
public func conditionalNavigationBarItems(_ condition: Bool, leading: AnyView, trailing: AnyView) -> some View {
Group {
if condition {
self.navigationBarItems(leading: leading, trailing: trailing)
} else {
self
}
}
}
}
struct ContentView: View {
#State private var showModal = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.showModal = true
}
NavigationLink("Push", destination: DetailView(isModal: false))
}
.navigationBarTitle("Home")
}
.sheet(isPresented: $showModal) {
NavigationView {
DetailView(isModal: true)
}
}
}
}
struct DetailView: View {
#Environment(\.presentationMode) private var presentationMode
let isModal: Bool
var body: some View {
Text("Hello World")
.navigationBarTitle(Text("Detail"), displayMode: .inline)
.navigationBarItems(trailing: deleteButton)
.conditionalNavigationBarItems(isModal, leading: closeButton, trailing: deleteButton)
}
private var closeButton: AnyView {
Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
Image(systemName: "xmark")
.frame(height: 36)
}.eraseToAnyView()
}
private var deleteButton: AnyView {
Button(action: { print("DELETE") }) {
Image(systemName: "trash")
.frame(height: 36)
}.eraseToAnyView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I don't see any trouble, just add Dismiss button to your navigation bar. You only have to rearrange your View hierarchy and there is no need to pass any binding to your DetailView
import SwiftUI
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
struct ContentView: View {
#State var sheet = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.sheet = true
}
NavigationLink("Push", destination: DetailView())
}.navigationBarTitle("Home")
}
.sheet(isPresented: $sheet) {
NavigationView {
DetailView().navigationBarTitle("Title").navigationBarItems(leading: Button(action: {
self.sheet.toggle()
}, label: {
Text("Dismiss")
}))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You still can dismiss it with swipe down, you can add some buttons (as part of DetailView declaration) ... etc.
When pushed, you have default back button, if shown modaly, you have dismiss
button indeed.
UPDATE (based od discussion)
.sheet(isPresented: $sheet) {
NavigationView {
GeometryReader { proxy in
DetailView().navigationBarTitle("Title")
.navigationBarItems(leading:
HStack {
Button(action: {
self.sheet.toggle()
}, label: {
Text("Dismiss").padding(.horizontal)
})
Color.clear
Button(action: {
}, label: {
Image(systemName: "trash")
.imageScale(.large)
.padding(.horizontal)
})
}.frame(width: proxy.size.width)
)
}
}
}
finally I suggest you to use
extension View {
#available(watchOS, unavailable)
public func navigationBarItems<L, T>(leading: L?, trailing: T) -> some View where L : View, T : View {
Group {
if leading != nil {
self.navigationBarItems(leading: leading!, trailing: trailing)
} else {
self.navigationBarItems(trailing: trailing)
}
}
}
}
Whenever we provide .navigationBarItems(leading: _anything_), ie anything, the standard back button has gone, so you have to provide your own back button conditionally.
The following approach works (tested with Xcode 11.2 / iOS 13.2)
.navigationBarItems(leading: Group {
if isModal {
closeButton
} else {
// custom back button here calling same dismiss
}
}, trailing: deleteButton)
Update: alternate approach might be as follows (tested in same)
var body: some View {
VStack {
if isModal {
Text("Hello")
.navigationBarItems(leading: closeButton, trailing: deleteButton)
} else {
Text("Hello")
.navigationBarItems(trailing: deleteButton)
}
}
.navigationBarTitle("Test", displayMode: .inline)
}

Resources