Wait for actionSheet result before proceeding? - swiftui

I have a button within a .contextMenu (ie: mark as paid)
The first thing it does is toggle on an actionSheet to select the payment method. Followed immediatly by some code to add an entry to users calendar via EventKit.
My problem is I want the actionSheet to return a result BEFORE the EventKit code is executed.
How do you make it wait?
Properties:
#State private var showPaymentSheet = false
#State private var paymentType = ""
var actionSheet: ActionSheet {
ActionSheet(title: Text("Payment Method"), buttons: [
.default(Text("💷 Cash")) { paymentType = "💷 Cash" },
.default(Text("💳 Bank Transfer")) { paymentType = "💳 Bank" },
.default(Text("🅿️ PayPal")) { paymentType = "🅿️ PayPal" },
.default(Text("🖋 Cheque")) { paymentType = "🖋 Cheque" },
.cancel()
])
}
Body:
.contextMenu {
Button(action: {
**// Run This ActionSheet FIRST and get result of \(paymentType) **
self.showPaymentSheet.toggle()
**// ONLY then continue with below **
let thisDay = self.selectDate.selectedDate
let eventStore = EventsRepository.shared.eventStore
let payment = EKEvent(eventStore: eventStore)
payment.calendar = event.calendar
payment.startDate = event.startDate
payment.endDate = event.endDate
payment.title = "💰\(event.title!)"
payment.notes = "\(event.notes!)\nPaid: \(Date().dateType(dateFormat: "EEE MMM dd yyyy # HH:mm b " )) by \(paymentType)"
payment.location = "\(event.location!)"
do {
try eventStore.save(payment, span: .thisEvent)//i - Save Updated (event)
try EventsRepository.shared.eventStore.remove(event, span: .thisEvent)
EventsRepository.shared.loadAndUpdateEvents(selectedDate: thisDay)
} catch {
print("Failled to cancel event")
}
NotificationCenter.default.post(name: .eventsDidChange, object: nil)
}){
HStack {
Text("Mark as Paid")
.font(Font.custom("ChalkboardSE-Bold", size: 32))
Image("payment")
.renderingMode(.original)
}
}.actionSheet(isPresented: $showPaymentSheet, content: {
self.actionSheet})
}

You can use onReceive.
Here is a simple demo:
struct ContentView: View {
#State private var showPaymentSheet = false
#State private var paymentType = ""
var body: some View {
Button("Choose payment") {
self.showPaymentSheet.toggle()
}
.actionSheet(isPresented: $showPaymentSheet) {
self.actionSheet
}
.onReceive(Just(paymentType)) { // <- add here
guard !$0.isEmpty else { return }
self.someOtherFunc()
}
}
var actionSheet: ActionSheet {
...
}
func someOtherFunc() {
print(paymentType)
}
}
However, I recommend moving logic to a separate class and keeping a View clean. Also why paymentType = ""? If it can be not set you probably should use nil instead. And why do you need it to be observable in the first place? You can also create a separate enum for payment types.
With above modifications, we can write an updated example:
enum PaymentType: String {
case cash = "💷 Cash"
case bank = "💳 Bank Transfer"
...
}
class PaymentHandler: ObservableObject {
func pay(_ paymentType: PaymentType) {
// print(paymentType.rawValue) // do something with `paymentType`
}
}
struct ContentView: View {
#ObservedObject private var vm = ViewModel()
#State private var showPaymentSheet = false
var body: some View {
Button("Choose payment") {
self.showPaymentSheet.toggle()
}
.actionSheet(isPresented: $showPaymentSheet) {
self.actionSheet
}
}
var actionSheet: ActionSheet {
ActionSheet(title: Text("Payment Method"), buttons: [
.default(Text(PaymentType.cash.rawValue)) { self.vm.pay(.cash) },
.default(Text(PaymentType.bank.rawValue)) { self.vm.pay(.bank) },
.cancel(),
])
}
}

Related

Struct not changing with Binding

I have this struct
struct MyObject {
var name:String
var color:String
var date:Date
init(name:String = "", color: String = "", date:Date = Date()) {
self.name = name
self.color = color
self.date = date
}
Then I have this on ContentView
#State private var temporaryObject = MyObject()
I send this to a view, like
var body: some View {
DoSomething($temporaryObject)
}
This is DoSomething
struct DoSomething: View {
#Binding var temporaryObject:MyObject
init(_ temporaryObject:Binding<MyObject>) {
self._temporaryObject = temporaryObject
}
var body: some View {
Button(action: {
// here is the problem
temporaryObject.name = "kkkk"
print(temporaryObject.name) // is equal to ""
}, label: {
Text("click me")
})
When I click the button, temporaryObject.name, in theory, is changed to "kkkk" but the print line shows it is still equals to empty.
why?
this example code works well for me. Does this code (taken from your question) not work for you?
struct ContentView: View {
#State private var temporaryObject = MyObject()
var body: some View {
VStack {
DoSomething(temporaryObject: $temporaryObject)
Text(temporaryObject.name) // <-- for testing
}
}
}
struct MyObject {
var name:String
var color:String
var date:Date
init(name:String = "", color: String = "", date:Date = Date()) {
self.name = name
self.color = color
self.date = date
}
}
struct DoSomething: View {
#Binding var temporaryObject:MyObject
var body: some View {
Button(action: {
temporaryObject.name = "kkkk"
print(temporaryObject.name) // is equal to "kkkk"
}, label: {
Text("click me")
})
}
}

Making data persist in Swift

I'm sorry if this is a naive question, but I need help getting this form to persist in core data. The variables are declared in the data model as strings. I simply cannot get this to cooperate with me. Also, the var wisconsin: String = "" is there because I can't call this view in my NavigationView without it throwing an error.
import SwiftUI
struct WisconsinToolOld: View {
//Variable
var wisconsin: String = ""
#Environment(\.managedObjectContext) private var viewContext
#State var saveInterval: Int = 5
var rateOptions = ["<12", ">12"]
#State var rate = ""
var body: some View {
List {
Section(header: Text("Spontaneous Respirations after 10 Minutes")) {
HStack {
Text("Respiratory Rate")
Spacer()
Picker("Rate", selection: $rate, content: {
ForEach(rateOptions, id: \.self, content: { rate in
Text(rate)
})
})
.pickerStyle(.segmented)
}
Section(header: Text("Result")) {
HStack {
Text("Raw Points")
Spacer()
Text("\(WisconsinToolInterpretation())")
}
}.navigationTitle("Wisconsin Tool")
}
}
func saveTool() {
do {
let wisconsin = Wisconsin(context: viewContext)
wisconsin.rate = rate
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
func WisconsinToolInterpretation() -> Int {
var points = 0
if rate == "<12" {
points += 3
}
else {
points += 1
}
return points
}
}

Published/Observed var not updating in view swiftui w/ called function

Struggling to get a simple example up and running in swiftui:
Load default list view (working)
click button that launches picker/filtering options (working)
select options, then click button to dismiss and call function with selected options (call is working)
display new list of objects returned from call (not working)
I'm stuck on #4 where the returned query isn't making it to the view. I suspect I'm creating a different instance when making the call in step #3 but it's not making sense to me where/how/why that matters.
I tried to simplify the code some, but it's still a bit, sorry for that.
Appreciate any help!
Main View with HStack and button to filter with:
import SwiftUI
import FirebaseFirestore
struct TestView: View {
#ObservedObject var query = Query()
#State var showMonPicker = false
#State var monFilter = "filter"
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Monday")
Spacer()
Button(action: {
self.showMonPicker.toggle()
}, label: {
Text("\(monFilter)")
})
}
.padding()
ScrollView(.horizontal) {
LazyHStack(spacing: 35) {
ForEach(query.queriedList) { menuItems in
MenuItemView(menuItem: menuItems)
}
}
}
}
.sheet(isPresented: $showMonPicker, onDismiss: {
//optional function when picker dismissed
}, content: {
CuisineTypePicker(selectedCuisineType: $monFilter)
})
}
}
The Query() file that calls a base query with all results, and optional function to return specific results:
import Foundation
import FirebaseFirestore
class Query: ObservableObject {
#Published var queriedList: [MenuItem] = []
init() {
baseQuery()
}
func baseQuery() {
let queryRef = Firestore.firestore().collection("menuItems").limit(to: 50)
queryRef
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.queriedList = querySnapshot?.documents.compactMap { document in
try? document.data(as: MenuItem.self)
} ?? []
}
}
}
func filteredQuery(category: String?, glutenFree: Bool?) {
var filtered = Firestore.firestore().collection("menuItems").limit(to: 50)
// Sorting and Filtering Data
if let category = category, !category.isEmpty {
filtered = filtered.whereField("cuisineType", isEqualTo: category)
}
if let glutenFree = glutenFree, !glutenFree {
filtered = filtered.whereField("glutenFree", isEqualTo: true)
}
filtered
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.queriedList = querySnapshot?.documents.compactMap { document in
try? document.data(as: MenuItem.self);
} ?? []
print(self.queriedList.count)
}
}
}
}
Picker view where I'm calling the filtered query:
import SwiftUI
struct CuisineTypePicker: View {
#State private var cuisineTypes = ["filter", "American", "Chinese", "French"]
#Environment(\.presentationMode) var presentationMode
#Binding var selectedCuisineType: String
#State var gfSelected = false
let query = Query()
var body: some View {
VStack(alignment: .center) {
//Buttons and formatting code removed to simplify..
}
.padding(.top)
Picker("", selection: $selectedCuisineType) {
ForEach(cuisineTypes, id: \.self) {
Text($0)
}
}
Spacer()
Button(action: {
self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text( "apply filters")
})
}
.padding()
}
}
I suspect that the issue stems from the fact that you aren't sharing the same instance of Query between your TestView and your CuisineTypePicker. So, when you start a new Firebase query on the instance contained in CuisineTypePicker, the results are never reflected in the main view.
Here's an example of how to solve that (with the Firebase code replaced with some non-asynchronous sample code for now):
struct MenuItem : Identifiable {
var id = UUID()
var cuisineType : String
var title : String
var glutenFree : Bool
}
struct ContentView: View {
#ObservedObject var query = Query()
#State var showMonPicker = false
#State var monFilter = "filter"
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Monday")
Spacer()
Button(action: {
self.showMonPicker.toggle()
}, label: {
Text("\(monFilter)")
})
}
.padding()
ScrollView(.horizontal) {
LazyHStack(spacing: 35) {
ForEach(query.queriedList) { menuItem in
Text("\(menuItem.title) - \(menuItem.cuisineType)")
}
}
}
}
.sheet(isPresented: $showMonPicker, onDismiss: {
//optional function when picker dismissed
}, content: {
CuisineTypePicker(query: query, selectedCuisineType: $monFilter)
})
}
}
class Query: ObservableObject {
#Published var queriedList: [MenuItem] = []
private let allItems: [MenuItem] = [.init(cuisineType: "American", title: "Hamburger", glutenFree: false),.init(cuisineType: "Chinese", title: "Fried Rice", glutenFree: true)]
init() {
baseQuery()
}
func baseQuery() {
self.queriedList = allItems
}
func filteredQuery(category: String?, glutenFree: Bool?) {
queriedList = allItems.filter({ item in
if let category = category {
return item.cuisineType == category
} else {
return true
}
}).filter({item in
if let glutenFree = glutenFree {
return item.glutenFree == glutenFree
} else {
return true
}
})
}
}
struct CuisineTypePicker: View {
#ObservedObject var query : Query
#Binding var selectedCuisineType: String
#State private var gfSelected = false
private let cuisineTypes = ["filter", "American", "Chinese", "French"]
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack(alignment: .center) {
//Buttons and formatting code removed to simplify..
}
.padding(.top)
Picker("", selection: $selectedCuisineType) {
ForEach(cuisineTypes, id: \.self) {
Text($0)
}
}
Spacer()
Button(action: {
self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text( "apply filters")
})
}
}

How to pass the initial value for a binding property?

I have this code:
struct MyView: View {
#State var fieldValue = 0
init(fieldValue:Int) {
self.fieldValue = fieldValue
}
var numberProxy: Binding<String> {
Binding<String>(
get: {
String(fieldValue)
},
set: {
fieldValue = Int($0) ?? 0
}
)
}
var body: some View {
TextField("", text: numberProxy,
onEditingChanged: { status in
},
onCommit:{
})
}
I call tis from another view with:
MyView(200)
but MyView always shows 0
How do I make the passed value show on what is a binding property?
This init is basically a dead end but it seems to be what you are asking for
struct MyViewParent: View {
var body: some View {
VStack{
//You will never receive anything back with this init
MyView(200)
}
}
}
struct MyView: View {
//State is a source of truth it will never relay something to a previous View
#State var fieldValue: Int //= 0 //Another init - Apple recommended
///Not a good way to init
init(_ fieldValue:Int) {
//You can init State here but there is no connection with the previous View
//This is not recommended per Apple documentation State should only accessed from a View body
//https://developer.apple.com/documentation/swiftui/state
self._fieldValue = State(initialValue: fieldValue)
}
//Binding is a 2-way connection
//https://developer.apple.com/documentation/swiftui/binding
var numberProxy: Binding<String> {
Binding<String>(
get: {
String(fieldValue)
},
set: {
fieldValue = Int($0) ?? 0
}
)
}
var body: some View {
VStack{
//Shows that your proxy updates the State
//Resets if a letter is put into the textfield.
Text(fieldValue.description)
TextField("", text: numberProxy, onEditingChanged: { status in }, onCommit:{ })
}
}
}
With this init you get the changes
struct MyViewParent: View {
#State var value: Int = 0
var body: some View {
VStack{
//Receives the changes from MyView
Text(value.description)
MyView(fieldValue: $value)
}
}
}
struct MyView: View {
//Binding is a 2-way connection
#Binding var fieldValue: Int
//Binding is a 2-way connection
//https://developer.apple.com/documentation/swiftui/binding
var numberProxy: Binding<String> {
Binding<String>(
get: {
String(fieldValue)
},
set: {
fieldValue = Int($0) ?? 0
}
)
}
var body: some View {
VStack{
//Shows that your proxy updates this View's Binding and parent State
//Resets to 0if a letter is put into the textfield.
Text(fieldValue.description)
TextField("", text: numberProxy, onEditingChanged: { status in }, onCommit:{ })
}
}
}
Use State(initialValue:).
struct MyViewTest55: View {
#State private var fieldValue = 0
init(fieldValue: Int) {
self._fieldValue = State(initialValue: fieldValue)
}
var numberProxy: Binding<String> {
Binding<String>(
get: {
String(fieldValue)
},
set: {
fieldValue = Int($0) ?? 0
}
)
}
var body: some View {
TextField("", text: numberProxy,
onEditingChanged: { status in
},
onCommit:{
})
}
}

SwiftUI Picker desn't bind with ObservedObject

I'm trying to fill up a Picker with data fetched asynchronously from external API.
This is my model:
struct AppModel: Identifiable {
var id = UUID()
var appId: String
var appBundleId : String
var appName: String
var appSKU: String
}
The class that fetches data and publish is:
class AppViewModel: ObservableObject {
private var appStoreProvider: AppProvider? = AppProvider()
#Published private(set) var listOfApps: [AppModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
self.loading = true
appStoreProvider?.dataProviderAppList { [weak self] (appList: [AppModel]) in
guard let self = self else {return}
DispatchQueue.main.async() {
self.listOfApps = appList
self.loading = false
}
}
}
init() {
fetchAppList()
}
}
The View is:
struct AppView: View {
#ObservedObject var appViewModel: AppViewModel = AppViewModel()
#State private var selectedApp = 0
var body: some View {
ActivityIndicatorView(isShowing: self.appViewModel.loading) {
VStack{
// The Picker doesn't bind with appViewModel
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName).tag(app.appName)
}
}
// The List correctly binds with appViewModel
List {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName.capitalized)
}
}
}
}
}
}
While the List view binds with the observed object appViewModel, the Picker doesn't behave in the same way. I can't realize why. Any help ?
I filed bug report, FB7670992. Apple responded yesterday, suggesting that I confirm this behavior in iOS 14, beta 1. It appears to now have been resolved.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Picker("", selection: $viewModel.wheelPickerValue) {
ForEach(viewModel.objects) { object in
Text(object.string)
}
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
}
Where
struct Object: Identifiable {
let id = UUID().uuidString
let string: String
}
class ViewModel: ObservableObject {
private var counter = 0
#Published private(set) var objects: [Object] = []
#Published var segmentedPickerValue: String = ""
#Published var wheelPickerValue: String = ""
fileprivate func nextSetOfValues() {
let newCounter = counter + 3
objects = (counter..<newCounter).map { value in Object(string: "\(value)") }
let id = objects.first?.id ?? ""
segmentedPickerValue = id
wheelPickerValue = id
counter = newCounter
}
init() {
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
self.nextSetOfValues()
}
timer.fire()
}
}
Results in:
I can't put this into your code because it is incomplete but here is a sample.
Pickers aren't meant to be dynamic. They have to be completely reloaded.
class DynamicPickerViewModel: ObservableObject {
#Published private(set) var listOfApps: [YourModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
loading = true
DispatchQueue.main.async() {
self.listOfApps.append(YourModel.addSample())
self.loading = false
}
}
init() {
fetchAppList()
}
}
struct DynamicPicker: View {
#ObservedObject var vm = DynamicPickerViewModel()
#State private var selectedApp = ""
var body: some View {
VStack{
//Use your loading var to reload the picker when it is done
if !vm.loading{
//Picker is not meant to be dynamic, it needs to be completly reloaded
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.vm.listOfApps){ app in
Text(app.name!).tag(app.name!)
}
}
}//else - needs a view while the list is being loaded/loading = true
List {
ForEach(self.vm.listOfApps){ app in
Text(app.name!.capitalized)
}
}
Button(action: {
self.vm.fetchAppList()
}, label: {Text("fetch")})
}
}
}
struct DynamicPicker_Previews: PreviewProvider {
static var previews: some View {
DynamicPicker()
}
}