How do I pass a var from one viewModel to a new viewModel in my .fullScreenCover() View? - swift

I'm building an app with swiftUI & firebase/firestore. I open a fullscreenCover sheet of a product selected from a catalog and the user can add it to a list below it. The product is selected on the previous page, and that is passed onto the .fullScreeenCover, where I'm introducing a new ViewModel for the list.
Where I'm getting confused: How do I pass the product ID passed into this fullScreenCover view into my newly introduced list's viewModel so that I can run the "add to list" function?
ViewModel for my List:
import SwiftUI
import Firebase
class ListViewModel: ObservableObject {
let var product: Product
#Published var userList = [List]()
#Published var list: List
init(list: List) {
self.list = list
}
func fetchList() {
let docRef = Firestore.firestore().collection("List")
guard let uid = AuthViewModel.shared.userSession?.uid else { return }
docRef.whereField("uid", isEqualTo: uid).getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.userList = documents.map({ List(dictionary: $0.data())} )}
}
func AddProductToList(product: Product, list: List) {
let listRef = Firestore.firestore().collection("List").document(list.id).collection("Products")
let productRef = Firestore.firestore().collection("Products").document(product.id)
productRef.getDocument { snapshot, _ in
listRef.document(self.product.id).setData([:]) { _ in
print("\(self.product.title) was saved to \(self.list.name)")
}
}
}
}
Code for .fullScreenCover() sheet View
import SwiftUI
struct ListCoverView: View {
#Binding var isPresented: Bool
let viewModel: LikeViewModel
#StateObject var listViewModel = ListViewModel(list)
var body: some View {
ZStack {
VStack (spacing: 10) {
VStack {
WebImage(url: URL(string: viewModel.product.image))
.resizable()
.frame(width: 220, height: 220)
.padding(.top,10)
Text("\(viewModel.product.title)")
.fontWeight(.bold)
.foregroundColor(.black)
.padding(.horizontal)
Text(viewModel.product.company)
.foregroundColor(.black)
.fontWeight(.bold)
}
.onAppear(perform: {
// Fetch products
listViewModel.fetchList()
})
ScrollView {
VStack {
Button(action: listViewModel.AddProductToList(Product)) {
ForEach(listViewModel.userList){list in
ListRow(list: listViewModel.list, viewModel: listViewModel)
}
}
}
}
}
}
Spacer()
Button(action: {isPresented.toggle()}, label : {
Text("Close")
.font(.system(size: 20))
.foregroundColor(.black)
})
.padding()
}
}
Parent View of the .fullScreenCover()
import SwiftUI
import SDWebImageSwiftUI
struct CardView: View {
let product: Product
#ObservedObject var viewModel: LikeViewModel
#State private var isShowingNewListSheet = false
init(product: Product) {
self.product = product
self.viewModel = LikeViewModel(product: product)
}
var body: some View {
VStack {
WebImage(url: URL(string: product.image))
.resizable()
.aspectRatio(contentMode: .fit)
Text(product.title)
.fontWeight(.bold)
.foregroundColor(.black)
.padding(.horizontal)
Text(product.company)
.foregroundColor(.black)
.fontWeight(.semibold)
.padding(.trailing,65)
HStack{
Button(action: {
viewModel.didLike ? viewModel.UnlikeProduct() : viewModel.LikeProduct()
}, label : {
Image(systemName: viewModel.didLike ? "heart.fill" : "heart")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(viewModel.didLike ? .red : .black)
})
.padding(.trailing,5)
Button(action: { isShowingNewListSheet.toggle()
}, label : {
Image(systemName: "square.and.arrow.down")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.black)
})
.fullScreenCover(isPresented: $isShowingNewListSheet) {
ListCoverView(isPresented: $isShowingNewListSheet, viewModel: viewModel)
}
}
}
.padding(.bottom)
.background(Color(.white))
.cornerRadius(15)
}
}

Related

How to save the answers of a specific user to his profile in the questionnaire app (SwiftUI)?

I am creating an app for parents or guardians of autistic children. In the app, you can create profiles for several kids and then click on the "Start Questionnaire" button in the personal card of each child. When the questionnaire page opens, there the parent answers several questions in several categories, where the questions from each category have a different weight (1, 2, 3, 4). At the end of the questionnaire, these answers are recorded in the profile of a particular kid in order to display a graph of his progress based on the answers on his personal page.
My question is that I can't figure out how to save answers to a specific kid's profile. To store profiles of children, I use CoreData, where there are four attributes: name, birthdate, dateCreated and answers. I assigned the Binary Data type to the latter. I created a #State variable in the QuestionnairePageView file and specified its type as [[String:Int]] to store a question category as a String (1, 2, 3 or 4) and an answer as an Int (yes = 1 or no = 0). Also in that file I created a saveAnswers function that assigns questionnaire answers to that #State variable when the user taps End Questionnaire button.
However, it didn't work. Please help me to understand the issue.
My data manager:
import SwiftUI
import CoreData
class CoreDataManager {
let container: NSPersistentContainer
static let shared: CoreDataManager = CoreDataManager()
private init() {
container = NSPersistentContainer(name: "PersonModel")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to initialize Core Data \(error)")
}
}
}
}
My QuestinarrieApp file:
import SwiftUI
#main
struct QuestinarrieApp: App {
let controller = CoreDataManager.shared.container
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, controller.viewContext)
}
}
}
My ContentView:
import SwiftUI
import CoreData
struct ContentView: View {
#State private var isShowingAddPersonPage: Bool = false
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(fetchRequest: Person.ownFetchRequest)
var persons: FetchedResults<Person>
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 10) {
ForEach(persons) { person in
let age = "\(calculateAge(fromDate: person.birthdate ?? Date()))"
NavigationLink {
PersonalPageView(person: person, name: person.name ?? "Noname", age: age)
} label: {
HStack {
Text(person.name ?? "Noname").font(.title2.bold())
Spacer()
Text(age + " years old").font(.body.bold())
}
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
.padding(.horizontal)
}
}
}
}
.navigationTitle("Persons")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isShowingAddPersonPage = true
}) {
Image(systemName: "plus.circle")
} //: BUTTON
.sheet(isPresented: $isShowingAddPersonPage) {
AddPersonView()
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let container = CoreDataManager.shared.container
ContentView()
.environment(\.managedObjectContext, container.viewContext)
}
}
My AddPersonView file:
import SwiftUI
struct AddPersonView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) private var viewContext
#State private var name: String = ""
#State private var birthdate: Date = Date()
var body: some View {
NavigationView {
VStack(spacing: 10) {
// MARK: - NAME
HStack(spacing: 12) {
Image(systemName: "person.text.rectangle")
.resizable()
.renderingMode(.template)
.scaledToFit()
.frame(width: 25, height: 25)
TextField("Name", text: $name)
} //: HSTACK
.padding(.horizontal)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
// MARK: - BIRTH DATE
HStack(spacing: 12) {
Image(systemName: "calendar")
.resizable()
.renderingMode(.template)
.scaledToFit()
.frame(width: 25, height: 25)
DatePicker(
"Birth date",
selection: $birthdate,
in: ...Date(),
displayedComponents: .date
)
.accentColor(.black)
} //: HSTACK
.padding(.horizontal)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
Spacer()
// MARK: - SAVE BUTTON
Button {
if name != "" {
savePerson()
presentationMode.wrappedValue.dismiss()
}
} label: {
Text("Save")
.font(.body.bold())
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
} //: BUTTON
}
.navigationTitle("Add Person")
.padding()
}
}
private func savePerson() {
let newPerson = Person(context: viewContext)
do {
newPerson.name = name
newPerson.birthdate = birthdate
newPerson.dateCreated = Date()
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
struct AddPersonView_Previews: PreviewProvider {
static var previews: some View {
let container = CoreDataManager.shared.container
AddPersonView()
.environment(\.managedObjectContext, container.viewContext)
}
}
My PersonalPageView:
import SwiftUI
struct PersonalPageView: View {
#Environment(\.managedObjectContext) private var viewContext
var person: Person
var name: String
var age: String
var body: some View {
VStack(spacing: 20) {
HStack {
Text(name).font(.largeTitle.bold())
Spacer()
Text(age + " years old").font(.title2.bold()).foregroundColor(.secondary)
}
RoundedRectangle(cornerRadius: 10)
.fill(.ultraThinMaterial)
.frame(height: 300)
VStack {
ForEach(person.answers) { answer in // <- This ForEach doesn't work for now
Text("\(answer)")
}
}
Spacer()
NavigationLink {
QuestinarriePageView(person: person, name: "Jack")
} label: {
Text("Start Questinarrie")
.font(.title2.bold())
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
}
}
.navigationBarTitleDisplayMode(.inline)
.padding()
}
}
struct PersonalPageView_Previews: PreviewProvider {
static var previews: some View {
PersonalPageView(person: Person(), name: "Igor", age: "28")
}
}
And finally QuestinarriePageView:
import SwiftUI
struct QuestinarriePageView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var answers: [[String:Int]] = [[:]]
var person: Person
var name: String
var body: some View {
VStack(spacing: 40) {
HStack {
Spacer()
Button {} label: {
Text("Quit").font(.title2)
}
.tint(.black)
}
VStack {
Text("Does \(name) like to play with other kids?")
HStack(spacing: 100) {
Button {
answers += [["1":1]]
} label: {
Text("Yes")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
Button {
answers += [["1":0]]
} label: {
Text("No")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
}
}
VStack {
Text("Does \(name) like to speak?")
HStack(spacing: 100) {
Button {
answers += [["2":1]]
} label: {
Text("Yes")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
Button {
answers += [["2":0]]
} label: {
Text("No")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
}
}
VStack {
Text("Does \(name) like to play dance?")
HStack(spacing: 100) {
Button {
answers += [["3":1]]
} label: {
Text("Yes")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
Button {
answers += [["3":0]]
} label: {
Text("No")
.foregroundColor(.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.secondary)
)
}
}
}
Spacer()
Button {
saveAnswers()
} label: {
Text("End Questionnaire")
.font(.body.bold())
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(Color(UIColor.secondarySystemBackground))
)
}
}
.toolbar(.hidden)
.padding()
}
private func saveAnswers() {
let thisPerson = person
do {
thisPerson.answers = answers as? Data
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
struct QuestinarriePageView_Previews: PreviewProvider {
static var previews: some View {
QuestinarriePageView(person: Person(), name: "Igor")
}
}
Also I have a file with some helpers:
import SwiftUI
import CoreData
func calculateAge(fromDate: Date) -> Int {
let now = Date()
let calendar = Calendar.current
let ageComponents = calendar.dateComponents([.year], from: fromDate, to: now)
let age = ageComponents.year!
return age
}
extension Person {
static var ownFetchRequest: NSFetchRequest<Person> {
let request: NSFetchRequest<Person> = Person.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "dateCreated", ascending: false)]
return request
}
}
I've been struggling with this problem for a month now. I hope this is solvableπŸ™

How to setup NavigationLink in SwiftUI sheet to redirect to new view

I am attempting to build a multifaceted openweathermap app. My app is designed to prompt the user to input a city name on a WelcomeView, in order to get weather data for that city. After clicking search, the user is redirected to a sheet with destination: DetailView, which displays weather details about that requested city. My goal is to disable dismissal of the sheet in WelcomeView and instead add a navigationlink to the sheet that redirects to the ContentView. The ContentView in turn is set up to display a list of the user's recent searches (also in the form of navigation links).
My issues are the following:
The navigationLink in the WelcomeView sheet does not work. It appears to be disabled. How can I configure the navigationLink to segue to destination: ContentView() ?
After clicking the navigationLink and redirecting to ContentView, I want to ensure that the city name entered in the WelcomeView textfield is rendered as a list item in the ContentView. For that to work, would it be necessary to set up an action in NavigationLink to call viewModel.fetchWeather(for: cityName)?
Here is my code:
WelcomeView
struct WelcomeView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State private var showingDetail: Bool = false
#State private var linkActive: Bool = true
#State private var acceptedTerms = false
var body: some View {
Section {
HStack {
TextField("Search Weather by City", text: $cityName)
.padding()
.overlay(RoundedRectangle(cornerRadius: 10.0).strokeBorder(Color.gray, style: StrokeStyle(lineWidth: 1.0)))
.padding()
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}
.sheet(isPresented: $showingDetail) {
VStack {
NavigationLink(destination: ContentView()){
Text("Return to Search")
}
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}.interactiveDismissDisabled(!acceptedTerms)
}
}
}.padding()
}
}
}
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
}
}
ContentView
let coloredToolbarAppearance = UIToolbarAppearance()
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
#State var showingDetail = false
init() {
// toolbar attributes
coloredToolbarAppearance.configureWithOpaqueBackground()
coloredToolbarAppearance.backgroundColor = .systemGray5
UIToolbar.appearance().standardAppearance = coloredToolbarAppearance
UIToolbar.appearance().scrollEdgeAppearance = coloredToolbarAppearance
}
var body: some View {
NavigationView {
VStack() {
List () {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")Β°").font(.system(size: 32))
}
}
}.onDelete { index in
self.viewModel.cityNameList.remove(atOffsets: index)
}
}.onAppear() {
viewModel.fetchWeather(for: cityName)
}
}.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
DetailView
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")Β°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
ViewModel
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}
Model
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DemoApp
#main
struct SwftUIMVVMWeatherDemoApp: App {
var body: some Scene {
WindowGroup {
// ContentView()
WelcomeView()
}
}
}

SwiftUI each item displays it own detailView when it is selected in a list

I'm trying to make each item display it own detailView in a list using SwiftUI. But for now, I got stuck because it only display the same detailView for any item. Would anyone know how to do that?
This is the code I have for now:
struct PastryListView: View {
#State private var isShowingDetailView = false
#State private var selectedPastry : Pastry?
#State private var selection: Int? = nil
var body: some View {
ZStack {
NavigationView {
List(MockData.pastries) { Pastry in
HStack {
Image(Pastry.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 180, height: 200)
VStack {
Text(Pastry.name)
.font(Font.custom("DancingScript-Regular", size: 30))
.fontWeight(.medium)
}
.padding(.leading)
}
.onTapGesture {
selectedPastry = Pastry
isShowingDetailView = true
}
}
.navigationTitle("πŸ₯ Pastries")
}
if isShowingDetailView { Pastry2DetailView(isShowingDetailView2: $isShowingDetailView, pastry: MockData.samplePastry2)
}
}
} }
there are many ways to achieve what you want, this is just one way:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
PastryListView()
}
}
}
struct Pastry: Identifiable {
var id: String = UUID().uuidString
var name: String
var image: UIImage
}
struct PastryListView: View {
#State private var pastries: [Pastry] = [
Pastry(name: "pastry1", image: UIImage(systemName: "globe")!),
Pastry(name: "pastry2", image: UIImage(systemName: "info")!)]
var body: some View {
NavigationView {
List(pastries) { pastry in
NavigationLink(destination: PastryDetailView(pastry: pastry)) {
HStack {
Image(uiImage: pastry.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80)
VStack {
Text(pastry.name)
.font(Font.custom("DancingScript-Regular", size: 30))
.fontWeight(.medium)
}
.padding(.leading)
}
}
}.navigationTitle("πŸ₯ Pastries")
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct PastryDetailView: View {
#State var pastry: Pastry
var body: some View {
Text("πŸ₯πŸ₯πŸ₯πŸ₯ " + pastry.name + " πŸ₯πŸ₯πŸ₯πŸ₯")
}
}

#Environment(\.presentationMode) var mode: Binding<PresentationMode> Messing other views

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.

Tabbar middle button utility function in SwiftUI

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()
}
}
}