I've been following a few tutorials online regarding setting up Storekit in my app. I've gotten as far as successfully requesting all the products and holding them in a products array. The next step of these tutorials is almost always listing out the products using a ForEach like so:
ForEach(products) { product in
Button {
Task.init {
try await purchaseProduct(product)
}
} label: {
HStack {
Text(product.displayName)
Spacer()
Text(product.displayPrice)
}
}
}
This doesn't work for my use case, unfortunately. The design I'm working off has 3 buttons in different parts of the screen, each of which initiate a purchase request for a different product.
I've managed to get some of the way there by doing this:
Button {
Task.init {
try await purchaseProduct(products.first!)
}
} label: {
HStack {
Text("\(products.first?.displayName ?? "No name")")
Spacer()
Text("\(products.first?.displayPrice ?? "No price")")
}
}
But this feels really hacky to me, for the following reasons:
Force unwrapping doesn't feel correct
I can make this work for the .first and .last item in the products array but I don't know how to get the second item, and this also means if the order of items inside products changes, my UI ties the wrong product to their respective button.
Here's my purchaseProduct function:
func purchaseProduct(_ product: Product) async throws -> StoreKit.Transaction {
let result = try await product.purchase()
switch result {
case .pending:
throw PurchaseError.pending
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
return transaction
case .unverified:
throw PurchaseError.failed
}
case .userCancelled:
throw PurchaseError.cancelled
#unknown default:
assertionFailure("Unexpected result")
throw PurchaseError.failed
}
}
Ideally, I'm looking to do something like this:
if let productOne = products.PRODUCTID {
Button {
Task.init {
try await purchaseProduct(productOne)
}
} label: {
HStack {
Text("\(productOne.displayName)")
Spacer()
Text("\(productOne.displayPrice)")
}
}
}
But I'm struggling to wrap my head around how to get there.
In order to achieve your desired if let productOne = products.PRODUCTID, you can use first(where:): https://developer.apple.com/documentation/swift/array/first(where:)
if let productOne = products.first(where: {$0.id == "iap.myapp.ProductOne"}) {
// ...
}
Related
I am trying to figure out how to display errors such as those found in the App Store Store class and Persistence.swift file in a single reporting module. Some of my errors within views use an environment object to tie the error production to the error reporting, but this isn’t feasible with errors in different classes.
So any pointers on handling these types of errors would be much appreciated.
Below is an example of my process to report an error within a view
// the save button has been pressed
struct saveButton: View {
#Environment(\.managedObjectContext) var viewContext
#EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
// prepping data for save to core data
do {
try viewContext.save()
} catch {
// report: Unable to Save Transaction
self.errorHandling.handle(error: AppError.saveTransactionError)
}
The App Store purchase method (below) may produce two errors that I would like to display with my app. This is the type of logic that I need help display the error within a view
#MainActor
func purchase() {
Task.init {
guard let product = products.first else {
return
}
guard AppStore.canMakePayments else {
return
}
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
// save to user defaults
self.purchasedIds = transaction.productID
break
case .unverified:
// throw PurchaseError.failed
// FIX: report error (Unable to purchase verification failed)
break
}
break
case .userCancelled:
break
// asked to buy (setting on phone prevents purchases)
case .pending:
break
default:
break
}
}
catch {
print(error.localizedDescription)
// FIX: report error (Unable to Complete Purchase)
}
}
}
enum AppError: LocalizedError {
case storePurchaseError
case storeVerificationError
var errorDescription: String? {
switch self {
case .storePurchaseError:
return NSLocalizedString("Store Purchase Error", comment: "")
case .storeVerificationError:
return NSLocalizedString("Store Verification Error", comment: "")
}
}
}
Below is some code I have been using to display an alert
struct ErrorAlert: Identifiable {
var id = UUID()
var message: String
var dismissAction: (() -> Void)?
}
class ErrorHandling: ObservableObject {
#Published var currentAlert: ErrorAlert?
func handle(error: Error) {
currentAlert = ErrorAlert(message: error.localizedDescription)
}
}
struct HandleErrorsByShowingAlertViewModifier: ViewModifier {
#StateObject var errorHandling = ErrorHandling()
func body(content: Content) -> some View {
content
.environmentObject(errorHandling)
.background(
EmptyView()
.alert(item: $errorHandling.currentAlert) { currentAlert in
Alert(
title: Text("Error"),
message: Text(currentAlert.message),
dismissButton: .default(Text("Ok")) {
currentAlert.dismissAction?()
}
)
}
)
}
}
extension View {
func withErrorHandling() -> some View {
modifier(HandleErrorsByShowingAlertViewModifier())
}
}
I am using Xcode 13.4 and trying to launch an async function when the Picker changes.
Picker(NSLocalizedString("Please choose a project", comment: ""), selection: $selectedProjectKey) {
ForEach(projectKeys, id: \.self) {
Text($0)
}
}
.font(.system(size: 14))
.onChange(of: selectedProjectKey, perform: { (selectedKey) in
print("selected key \(selectedKey)")
async {
do {
let projectTasks = try await api.getProjectTasksByProjectKey(projectKey: selectedKey)
projectTasksKeys = projectTasks.map{$0.key}
} catch {
/// To define error behavour
}
}
})
It works, but I know that async is deprecated (yellow warning).
That's why, I tried with Task.init {... } or Task { ... } and instead, I get an error:
"Trailing closure passed to parameter of type 'Decoder' that does not
accept a closure".
I suppose that I did something definitely wrong but I can't understand what it is and what difference there is between async{...} and Task.init { ... } .
I am trying to change the swipeAction from "Paid" to "UnPaid" based on payment status and somehow seems to be failing. Error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
Appreciate any help
struct ContentView: View {
var data: [Data] = [data1, data2, data3, data4]
#State var swipeLabel = true
var body: some View {
let grouped = groupByDate(data)
List {
ForEach(Array(grouped.keys).sorted(by: >), id: \.self) { date in
let studentsDateWise = grouped[date]!
Section(header:Text(date, style: .date)) {
ForEach(studentsDateWise, id:\.self) { item in
HStack {
Text(item.name)
padding()
Text(item.date, style: .time)
if(item.paymentStatus == false) {
Image(systemName: "person.fill.questionmark")
.foregroundColor(Color.red)
} else {
Image(systemName: "banknote")
.foregroundColor(Color.green)
}
} // HStack ends here
.swipeActions() {
if(item.paymentStatus) {
Button("Paid"){}
} else {
Button("UnPaid"){}
}
}
} // ForEach ends here...
} // section ends here
} // ForEach ends here
} // List ends here
} // var ends here
}
The body func shouldn't do any grouping or sorting. You need to prepare your data first into properties and read from those in body, e.g. in an onAppear block. Also if your Data is a struct you can't use id: \.self you need to either specify a unique identifier property on the data id:\.myUniqueID or implement the Indentifiable protocol by either having an id property or an id getter that computes a unique identifier from other properties.
I would suggest separating all this code into small Views with a small body that only uses one or a two properties. Work from bottom up. Then eventually with one View works on an array of dates and another on an array of items that contains the small Views made earlier.
You should probably also learn that if and foreach in body are not like normal code, those are converted into special Views. Worth watching Apple's video Demystify SwiftUI to learn about structural identity.
OK before anymore marks this as duplicate, I have looked at posts similar to this on StackOverFlow and even the Apple forums, and implemented all recommended solutions, yet the problem persists. I've using a LazyVGrid. My code is as follows:
This is my custom scroll view that allows for pagination once the user reaches the end of the scroll:
if !quickSearchViewModel.isLoading && !quickSearchViewModel.search_result.isEmpty {
DataFetchingScrollView(.vertical, alignment: .center, onOffsetChange: { (off, height) in
offset = off
heightMinusOffset = height
if heightMinusOffset <= UIScreen.main.bounds.height &&
!quickSearchViewModel.search_result.isEmpty {
quickSearchViewModel.paginate_searchterm {
print("Paginated")
} onError: { (error) in
print(error)
}
}
}) {
createGrid()
.id(UUID())
}
And these are my two functions to create the grid and the view inside the grid:
private func createGrid() -> some View {
LazyVGrid(columns: columns) {
ForEach(quickSearchViewModel.search_result, id: \.product_uid) { product in
createProductItemView(product)
.id(product.product_uid)
}
}
}
private func createProductItemView(_ product: ProductModel) -> some View {
ProductItemView(product: product)
.id(product.product_uid)
}
Yes, I know I have spammed the id, but I've added '.id' to all views indivdually and the problem persists. It's as soon as I hit search, the content loads and appears in the grid and thats when my application crashes.
Edit - the product.product_uid is an auto-id generated by Firebase. I am literally using this same method in other views, and some work without issues, others may have a little bug.
Found this issue, it was because I was using orderBy when querying data.
I have an app with a number of categories--some the can be changed by the user and some that can't. The categories that can't be changed are stored currently in an array while categories that the user can change are stored in a class. The problem comes when creating expense entries where the picker needs to show both types of categories.
The other side of the coin is to place all the categories (text strings) in the class. Here the expense entry picker and class storage will work ok, but then there is the problem of preventing the user from deleting the default categories.
I'm guessing that latter option is the better route since it will place all the categories in the picker list. Here is the code for storing the dynamic categories. I suppose I could add some init() code to store the categories that don't change. Not sure exactly how to do that.
struct CatItem: Codable {
var catName: String
var catPix: String
}
class Categories: ObservableObject {
#Published var catItem: [CatItem] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(catItem) {
UserDefaults.standard.set(encoded, forKey: "workCat")
}
}
}
init() {
if let catItem = UserDefaults.standard.data(forKey: "workCat") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([CatItem].self, from: catItem) {
self.catItem = decoded
return
}
}
self.catItem = []
}
}
How would you prevent the user from deleting some of the fixed categories? Usually you have a list with an onDelete statement.
Can you use the index to determine if deleting is allowed? For example don't delete entry if indexSet[index] < 8?
.onDelete { indexSet in
for index in indexSet {
remove entry
}
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
I assume you wanted something like
.onDelete { indexSet in
guard let i = indexSet.first, indexSet[i] < 8 else { return }
// .. other code
I resolved this storage quandary by deciding to store both the permanent and changeable categories in the same class catItem. In the module where the categories may be viewed and where new categories may be added I use a filtered list showing only the categories that may be edited.
This is how the categories are initialized at startup:
let item = CatItem(catName: "name1", catPix: "sf symbol", noShow: true)
self.catItem.append(item)
This is how the categories are displayed in a list:
List {
ForEach(categories.catItem, id: \.catName) { item in
if item.noShow == false {
HStack {
Text(item.catName)
.padding(.horizontal, 10)
Spacer()
Image(systemName: item.catPix).resizable()
.frame(width: 30, height: 30)
}
}
}
.onDelete(perform: removeItems)
}
So if the user can't see the permanent categories in List, then they can't delete them. The user can only delete the categories that they add.
When the user adds new categories the noShow parameter is set to false.
In the picker the categories are not filtered so all categories may be viewed.