My goal is to have the user click a button that then gives them a choice of yes or cancel, this works fine. Is yes is selected it should move to the Camera View. I am getting the error: Result of 'NavigationLink<Label, Destination>' initializer is unused.
struct ContentView: View {
#State private var showAlert = false
var body: some View {
NavigationStack
{
VStack {
Button{
showAlert = true
} label: {
Text("+")
}
.frame(width: 40, height: 40)
.symbolVariant(.fill)
.background(.red)
.cornerRadius(15)
.foregroundColor(.white)
.padding(.trailing,300)
Spacer()
}
.alert("Create Event Here?", isPresented: $showAlert) {
Button("Yes"){NavigationLink("addCameraView", destination: CameraView())//*****gets the error
}
Button("Cancel", role: .cancel) { }
}
}
struct CameraView: View{
#State private var sourceType: UIImagePickerController.SourceType = .photoLibrary
#State private var selectedImage: UIImage?
#State private var imagePickerDisplay = false
var body: some View {
NavigationView {
VStack {
if selectedImage != nil {
Image(uiImage: selectedImage!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 300, height: 300)
} else {
Image(systemName: "snow")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 300, height: 300)
}
Button("Camera") {
self.sourceType = .camera
self.imagePickerDisplay.toggle()
}.padding()
}
.navigationBarTitle("Take a Photo of the Event")
.sheet(isPresented: self.$imagePickerDisplay) {
ImagePickerView(selectedImage: self.$selectedImage, sourceType: self.sourceType)
}
}
}
}
}
To navigate with a button, we need to utilize a variable as our trigger. Wrapping the button in a NavigationLink and updating the associated variable to the appropriate value will trigger the navigation.
Below you will find the updated ContentView. CameraView remains unchanged.
import SwiftUI
struct ContentView: View {
#State private var showAlert = false
#State var selection: Int? = nil
var body: some View {
NavigationStack
{
VStack {
Button{
showAlert = true
} label: {
Text("+")
}
.frame(width: 40, height: 40)
.symbolVariant(.fill)
.background(.red)
.cornerRadius(15)
.foregroundColor(.white)
.padding(.trailing,300)
Spacer()
}
.alert("Create Event Here?", isPresented: $showAlert) {
NavigationLink(destination: CameraView(), tag: 1, selection: $selection) {
Button("Yes"){
selection = 1
}
}
Button("Cancel", role: .cancel) {
selection = nil
}
}
}
}
}
Related
Essentially I would like to trigger the MainButton in ContentView to Open SheetView and then use NavCloseButton to Close the SheetView to Return Back to ContentView. I've been trying to do this using #State and #Binding. While getting SheetView presented using .sheet(isPresented: is simple I'm having trouble dismissing it when the buttons are extracted out.
Can someone please show example how these actions would be performed?
ContentView:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
MainButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer")
}
.navigationTitle("Page One")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.preferredColorScheme(.dark)
}
}
SheetView:
struct SheetView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(trailing: NavCloseButton(color: .red,
title: "Close",
image: ""))
}
}
}
struct SheetView_Previews: PreviewProvider {
static var previews: some View {
SheetView()
.preferredColorScheme(.dark)
}
}
AppButtons:
struct NavCloseButton: View {
var color: Color
var title: String
var image: String
var body: some View {
Button {
print("Closing")
} label: {
Text(title)
.padding()
.frame(width: 100, height: 40)
.background(color)
.foregroundColor(.white)
.cornerRadius(10)
.font(.system(.body))
}
}
}
struct MainButton: View {
var color: Color
var title: String
var image: String
var body: some View {
Button {
print("Opened")
} label: {
Label {
Text(title)
} icon: {
Image(systemName: image)
}
.padding()
.frame(width: 350, height: 60)
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
.font(.title2)
}
}
}
Following your code this is how you would do it.
Changes are commented.
struct ContentView: View {
#State private var showSheet = false // define state
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
MainButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer", showSheet: $showSheet) // pass binding
}
.navigationTitle("Page One")
.sheet(isPresented: $showSheet) { // present sheet
SheetView(showSheet: $showSheet) // pass binding
}
}
}
}
struct SheetView: View {
#Binding var showSheet: Bool // pass binding here
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(
trailing:
NavCloseButton(color: .red,
title: "Close",
image: "",
showSheet: $showSheet)) // pass binding
}
}
}
struct NavCloseButton: View {
var color: Color
var title: String
var image: String
#Binding var showSheet: Bool // pass binding here
var body: some View {
Button {
print("Closing")
showSheet = false // set binding to dismiss sheet
} label: {
Text(title)
.padding()
.frame(width: 100, height: 40)
.background(color)
.foregroundColor(.white)
.cornerRadius(10)
.font(.system(.body))
}
}
}
struct MainButton: View {
var color: Color
var title: String
var image: String
#Binding var showSheet: Bool // pass binding here
var body: some View {
Button {
print("Opened")
showSheet = true // set binding to show sheet
} label: {
Label {
Text(title)
} icon: {
Image(systemName: image)
}
.padding()
.frame(width: 350, height: 60)
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
.font(.title2)
}
}
}
But it might be easier to change the buttons to vars or define custom Button styles.
Here is another variant with one CutomButton that is taking a button action as a closure, reducing the Binding passing around:
struct ContentView: View {
#State private var showSheet = false // define state
var body: some View {
NavigationView {
VStack {
Spacer()
Image(systemName: "hand.thumbsup.circle.fill")
.resizable()
.frame(width: 200, height: 200)
.symbolRenderingMode(.hierarchical)
Spacer()
CustomButton(color: .blue, title: "Tap to Open", image: "lock.open.laptopcomputer") {
showSheet = true // passed button action
}
.font(.title2)
}
.navigationTitle("Page One")
.sheet(isPresented: $showSheet) { // present sheet
SheetView(showSheet: $showSheet) // pass binding
}
}
}
}
struct SheetView: View {
#Binding var showSheet: Bool // pass binding here
var body: some View {
NavigationView {
VStack {
Text("Hello, World!")
}
.navigationTitle("Sheet View")
.navigationBarItems(
trailing:
CustomButton(color: .red, title: "Close") {
showSheet = false // passed button action
}
)
}
}
}
struct CustomButton: View {
let color: Color
let title: String
var image: String? = nil
let action: () -> Void // button action (as trailing closure)
var body: some View {
Button {
action() // run the passed action
} label: {
Group {
if let image {
Label(title, systemImage: image)
} else {
Text(title)
}
}
.padding()
.background(color)
.foregroundColor(.white)
.cornerRadius(15)
}
}
}
I have 2 user view called user1 and user2, I am updating user with button, and I want give a transition animation to update, but for some reason my transition does not work, as I wanted, the issue is there that Text animated correctly but image does not, it stay in its place and it does not move with Text to give a smooth transition animation.
struct ContentView: View {
#State var show: Bool = Bool()
var body: some View {
VStack {
if (show) {
UserView(label: { Text("User 1") })
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.trailing), removal: AnyTransition.move(edge: Edge.leading)))
}
else {
UserView(label: { Text("User 2") })
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.leading), removal: AnyTransition.move(edge: Edge.trailing)))
}
Button("update") { show.toggle() }
}
.padding()
.animation(Animation.linear(duration: 1.0), value: show)
}
}
struct UserView<Label: View>: View {
let label: () -> Label
#State private var heightOfLabel: CGFloat? = nil
var body: some View {
HStack {
if let unwrappedHeight: CGFloat = heightOfLabel {
Image(systemName: "person")
.resizable()
.frame(width: unwrappedHeight, height: unwrappedHeight)
}
label()
.background(GeometryReader { proxy in
Color.clear
.onAppear(perform: { heightOfLabel = proxy.size.height })
})
Spacer(minLength: CGFloat.zero)
}
.animation(nil, value: heightOfLabel)
}
}
the heightOfLabel doesn't have to be optional, and then it works:
struct UserView<Label: View>: View {
let label: () -> Label
#State private var heightOfLabel: CGFloat = .zero // not optional
var body: some View {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: heightOfLabel, height: heightOfLabel)
label()
.background(GeometryReader { proxy in
Color.clear
.onAppear(perform: { heightOfLabel = proxy.size.height })
})
Spacer(minLength: CGFloat.zero)
}
.animation(nil, value: heightOfLabel)
}
}
My App currently has two pages, first page has a circle plus button which could lead us to a second page. Basically, I have a save button which after clicking it, we could get back to the rood page. I followed this link for going back to root view. I tried the most up voted code, his code works perfectly. I reduced his code to two scene (basically the same scenario as mine), which also works perfectly. But then I don't know why my own code, pasted below, doesn't work. Basically my way of handling going back to root view is the same as the one in the link.
//
// ContentView.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/22/20.
//
import SwiftUI
import UIKit
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
struct ContentView: View {
#EnvironmentObject private var fridge : Fridge
private var dbStartWith=0;
#State var pushed: Bool = false
#State private var selection = 1;
#State private var addFood = false;
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(1)
Text("random tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("profile")
}
.tag(0)
}.environmentObject(fridge)
}
}
struct FoodView: View{
var body: some View{
NavigationView{
Text("food destination view ");
}
}
}
struct AddFoodView: View{
#Binding var pushed : Bool
#EnvironmentObject private var fridgeView : Fridge
#State private var name = ""
#State private var count : Int = 1
#State private var category : String = "肉类";
#State var showCategory = false
#State var showCount = false
var someNumberProxy: Binding<String> {
Binding<String>(
get: { String(format: "%d", Int(self.count)) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.count = value.intValue;
}
}
)
}
var body: some View{
ZStack{
NavigationView{
VStack{
Button (action: {
self.pushed = false ;
//let tempFood=Food(id: fridgeView.index!,name: name, count: count, category: category);
//fridgeView.addFood(food: tempFood);
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
ZStack{
if self.showCount{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCount=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $count,label: EmptyView()) {
ForEach(1..<100){ number in
Text("\(number)").tag("\(number)")
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
.offset(x:10,y:-10)
Spacer()
}
if self.showCategory{
let categoryArr = ["肉类","蔬菜类","饮料类","调味品类"]
ZStack{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCategory=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $category,label: EmptyView()) {
ForEach(0..<categoryArr.count){ number in
Text(categoryArr[number]).tag(categoryArr[number])
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
Spacer()
}.offset(x:10,y:20)
}
}
}.animation(.easeInOut)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you read my code carefully, there are some variables are missing referencing. That's because I pasted part of the code that relates to my issue.
Food Class
//
// Food.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/23/20.
//
import Foundation
class Food: Identifiable {
init(id:Int, name: String, count: Int, category: String){
self.id=id;
self.name=name;
self.count=count;
self.category=category;
}
var id: Int
var name: String
var count: Int
var category: String
}
Fridge class
//
// Fridge.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/27/20.
//
import Foundation
class Fridge: ObservableObject{
init(){
db=DBhelper();
let result = setIndex(database: db!);
self.index = result.1;
self.container=result.0;
}
var db:DBhelper?
var index : Int?
#Published var container : [Food]?;
func setIndex(database: DBhelper) -> ([Food],Int){
let foodList : [Food] = database.read();
var index=0;
for food in foodList{
index = max(food.id,index);
}
return (foodList,(index+1));
}
func updateindex(index: inout Int){
index=index+1;
}
func testExist(){
if let data = db {
print("hi")
}
else{
print("doesnt exist")
}
}
func addFood(food:Food){
self.db!.insert(id: self.index!, name: food.name, count:food.count, category: food.category);
self.container!.append(food);
}
}
Because you implemented a new NaviagtionView in AddFoodView. Simply remove this and it should work. Look at the link you provided. There is no NavigationView in the child.
Correct me if Im wrong but the core code parts here that produce this issue are as follows:
Here you start:
struct ContentView: View {
#State var pushed: Bool = false
// Deleted other vars
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
// Here you navigate to the child view
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
Here you land and want to go back to root:
struct AddFoodView: View{
#Binding var pushed : Bool
// Deleted the other vars for better view
var body: some View{
ZStack{
NavigationView{ // <-- remove this
VStack{
Button (action: {
// here you'd like to go back
self.pushed = false;
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
For the future:
I have the feeling you might have troubles with the navigation in general.
Actually it is really simple:
You implement one NavigationView at the "root" / start of your navigation.
From there on you only use NavigationLinks to go further down to child pages. No NavigationView needed anymore.
I have a MailView()
import Foundation
import SwiftUI
import UIKit
import MessageUI
struct MailView: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentation
#Binding var result: Result<MFMailComposeResult, Error>?
let newSubject : String
let newMsgBody : String
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var presentation: PresentationMode
#Binding var result: Result<MFMailComposeResult, Error>?
init(presentation: Binding<PresentationMode>,
result: Binding<Result<MFMailComposeResult, Error>?>) {
_presentation = presentation
_result = result
}
func mailComposeController(_ controller: MFMailComposeViewController,
didFinishWith result: MFMailComposeResult,
error: Error?) {
defer {
$presentation.wrappedValue.dismiss()
}
guard error == nil else {
self.result = .failure(error!)
return
}
self.result = .success(result)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentation: presentation,
result: $result)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.mailComposeDelegate = context.coordinator
vc.setToRecipients(["hello#email.co.uk"])
vc.setSubject(newSubject)
vc.setMessageBody(newMsgBody, isHTML: false)
return vc
}
func updateUIViewController(_ uiViewController: MFMailComposeViewController,
context: UIViewControllerRepresentableContext<MailView>) {
}
}
And In my SettingViews its called like so:
import SwiftUI
import URLImage
import UIKit
import MessageUI
struct SettingsView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#State private var showMailSheet = false
#State var result: Result<MFMailComposeResult, Error>? = nil
#State private var subject: String = ""
#State private var emailBody: String = ""
#EnvironmentObject var session: SessionStore
var body: some View {
NavigationView {
VStack(alignment: .leading) {
List {
Section(header: Text("Account")) {
NavigationLink(destination: ProfileView()) {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Edit Profile").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
NavigationLink(destination: AccountView()) {
HStack {
Image(systemName: "doc")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("View Account").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
NavigationLink(destination: PreferencesView()) {
HStack {
Image(systemName: "slider.horizontal.3")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Preferences").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
}
Section(header: Text("Support")) {
HStack {
Image(systemName: "bubble.right")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Contact Us").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
self.subject = "Hello"
self.sendEmail()
}) {
Text("Send").font(.system(size:12))
}
}
HStack {
Image(systemName: "ant")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Report An Issue").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
self.sendEmail()
self.subject = "Report Issue"
self.emailBody = "Im having the following issues:"
}) {
Text("Report").font(.system(size:12))
}
}
}
Section (header: Text("Legal")) {
HStack {
Image(systemName: "hand.raised")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Privacy Policy").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
if let url = URL(string: "http://www.mysite.co.uk/privacy.html") {
UIApplication.shared.open(url)
}
}) {
Text("View").font(.system(size:12))
}
}
HStack {
Image(systemName: "folder")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Terms and Conditions (EULA)").font(.callout).fontWeight(.medium)
}
Spacer()
Button(action: {
if let url = URL(string: "http://www.mysite.co.uk/eula.html") {
UIApplication.shared.open(url)
}
}) {
Text("View").font(.system(size:12))
}
}
}
}.listStyle(GroupedListStyle())
}.navigationBarTitle("Settings", displayMode: .inline)
.background(NavigationBarConfigurator())
}.sheet(isPresented: $showMailSheet) {
MailView(result: self.$result, newSubject: self.subject, newMsgBody: self.emailBody)
}
}
func sendEmail() {
if MFMailComposeViewController.canSendMail() {
self.showMailSheet = true
} else {
print("Error sending mail")
}
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
}
}
My Sheet is appearing nicely and once the email is sent, the sheet is dismissed as expected, but the following is causing an issue:
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
When I click:
NavigationLink(destination: ProfileView()) {
HStack {
Image(systemName: "person")
.resizable()
.frame(width: 20, height: 20)
VStack(alignment: .leading) {
Text("Edit Profile").font(.callout).fontWeight(.medium)
}
}.padding([.top,.bottom],5).padding(.trailing,10)
}
There is a action sheet:
.actionSheet(isPresented: self.$profileViewModel.showActionSheet){
ActionSheet(title: Text("Add a profile image"), message: nil, buttons: [
.default(Text("Camera"), action: {
self.profileViewModel.showImagePicker = true
self.sourceType = .camera
}),
.default(Text("Photo Library"), action: {
self.profileViewModel.showImagePicker = true
self.sourceType = .photoLibrary
}),
.cancel()
])
}.sheet(isPresented: self.$profileViewModel.showImagePicker){
imagePicker(image: self.$profileViewModel.upload_image, showImagePicker: self.$profileViewModel.showImagePicker, sourceType: self.sourceType)
}
When i click this button it keeps dismissing the button and I can't click on the options presented.
Any idea how I can have the #Environment(\.presentationMode) var mode: Binding<PresentationMode> only effecting the dismissing of the email? and not interfering with anything else?
#Environment(\.presentationMode) should be used for the last child view that you want to have this custom behaviour.
Any child view from where you declared the #Environment(\.presentationMode), will also inherit the same behaviour.
If you declare it only in MailView, it should fix it.
I'm trying to reproduce a "Instagram" like tabBar which has a "Utility" button in the middle which doesn't necessarily belong to the tabBar eco system.
I have attached this gif to show the behaviour I am after. To describe the issue. The tab bar in the middle (Black plus) is click a ActionSheet is presented INSTEAD of switching the view.
How I would do this in UIKit is simply use the
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
print("Selected item")
}
Function from the UITabBarDelegate. But obviously we can't do this in SwiftUI so was looking to see if there was any ideas people have tried. My last thought would be to simply wrap it in a UIView and use it with SwiftUI but would like to avoid this and keep it native.
I have seen a write up in a custom TabBar but would like to use the TabBar provided by Apple to avoid any future discrepancies.
Thanks!
Edit: Make the question clearer.
Thanks to Aleskey for the great answer (Marked as correct). I evolved it a little bit in addition to a medium article that was written around a Modal. I found it to be a little different
Here's the jist.
A MainTabBarData which is an Observable Object
final class MainTabBarData: ObservableObject {
/// This is the index of the item that fires a custom action
let customActiontemindex: Int
let objectWillChange = PassthroughSubject<MainTabBarData, Never>()
var previousItem: Int
var itemSelected: Int {
didSet {
if itemSelected == customActiontemindex {
previousItem = oldValue
itemSelected = oldValue
isCustomItemSelected = true
}
objectWillChange.send(self)
}
}
func reset() {
itemSelected = previousItem
objectWillChange.send(self)
}
/// This is true when the user has selected the Item with the custom action
var isCustomItemSelected: Bool = false
init(initialIndex: Int = 1, customItemIndex: Int) {
self.customActiontemindex = customItemIndex
self.itemSelected = initialIndex
self.previousItem = initialIndex
}
}
And this is the TabbedView
struct TabbedView: View {
#ObservedObject private var tabData = MainTabBarData(initialIndex: 1, customItemIndex: 2)
var body: some View {
TabView(selection: $tabData.itemSelected) {
Text("First Screen")
.tabItem {
VStack {
Image(systemName: "globe")
.font(.system(size: 22))
Text("Profile")
}
}.tag(1)
Text("Second Screen")
.tabItem {
VStack {
Image(systemName: "plus.circle")
.font(.system(size: 22))
Text("Profile")
}
}.tag(2)
Text("Third Screen")
.tabItem {
VStack {
Image(systemName: "number")
.font(.system(size: 22))
Text("Profile")
}
}.tag(3)
}.actionSheet(isPresented: $tabData.isCustomItemSelected) {
ActionSheet(title: Text("SwiftUI ActionSheet"), message: Text("Action Sheet Example"),
buttons: [
.default(Text("Option 1"), action: option1),
.default(Text("Option 2"), action: option2),
.cancel(cancel)
]
)
}
}
func option1() {
tabData.reset()
// ...
}
func option2() {
tabData.reset()
// ...
}
func cancel() {
tabData.reset()
}
}
struct TabbedView_Previews: PreviewProvider {
static var previews: some View {
TabbedView()
}
}
Similar concept, just uses the power of SwiftUI and Combine.
You could introduce new #State property for storing old tag of presented tab. And perform the next method for each of your tabs .onAppear { self.oldSelectedItem = self.selectedItem } except the middle tab. The middle tab will be responsible for showing the action sheet and its method will look the following:
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Working example:
import SwiftUI
struct ContentView: View {
#State private var selectedItem = 1
#State private var shouldShowActionSheet = false
#State private var oldSelectedItem = 1
var body: some View {
TabView (selection: $selectedItem) {
Text("Home")
.tabItem { Image(systemName: "house") }
.tag(1)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Search")
.tabItem { Image(systemName: "magnifyingglass") }
.tag(2)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Add")
.tabItem { Image(systemName: "plus.circle") }
.tag(3)
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Text("Heart")
.tabItem { Image(systemName: "heart") }
.tag(4)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Profile")
.tabItem { Image(systemName: "person.crop.circle") }
.tag(5)
.onAppear { self.oldSelectedItem = self.selectedItem }
}
.actionSheet(isPresented: $shouldShowActionSheet) { ActionSheet(title: Text("Title"), message: Text("Message"), buttons: [.default(Text("Option 1"), action: option1), .default(Text("Option 2"), action: option2) , .cancel()]) }
}
func option1() {
// do logic 1
}
func option2() {
// do logic 2
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Previous answers did not help me so I'm pasting my complete solution.
import SwiftUI
import UIKit
enum Tab {
case map
case recorded
}
#main
struct MyApp: App {
#State private var selectedTab: Tab = .map
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
VStack {
switch selectedTab {
case .map:
NavigationView {
FirstView()
}
case .recorded:
NavigationView {
SecondView()
}
}
CustomTabView(selectedTab: $selectedTab)
.frame(height: 50)
}
}
}
}
struct FirstView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("First view")
}
}
struct SecondView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("second view")
}
}
struct CustomTabView: View {
#Binding var selectedTab: Tab
var body: some View {
HStack {
Spacer()
Button {
selectedTab = .map
} label: {
VStack {
Image(systemName: "map")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Map")
.font(.caption2)
}
.foregroundColor(selectedTab == .map ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
Button {
} label: {
ZStack {
Circle()
.foregroundColor(.secondary)
.frame(width: 80, height: 80)
.shadow(radius: 2)
Image(systemName: "plus.circle.fill")
.resizable()
.foregroundColor(.primary)
.frame(width: 72, height: 72)
}
.offset(y: -2)
}
Spacer()
Button {
selectedTab = .recorded
} label: {
VStack {
Image(systemName: "chart.bar")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Recorded")
.font(.caption2)
}
.foregroundColor(selectedTab == .recorded ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
}
}
}