SwiftUI: Difference between async{} and Task.init{ } - swiftui

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 { ... } .

Related

How to bind to data that's part of an optional in SwiftUI

I'm curious, how do we specify a binding to State data that is part of an optional? For instance:
struct NameRecord {
var name = ""
var isFunny = false
}
class AppData: ObservableObject {
#Published var nameRecord: NameRecord?
}
struct NameView: View {
#StateObject var appData = AppData()
var body: some View {
Form {
if appData.nameRecord != nil {
// At this point, I *know* that nameRecord is not nil, so
// I should be able to bind to it.
TextField("Name", text: $appData.nameRecord.name)
Toggle("Is Funny", isOn: $appData.nameRecord.isFunny)
} else {
// So far as I can tell, this should never happen, but
// if it does, I will catch it in development, when
// I see the error message in the constant binding.
TextField("Name", text: .constant("ERROR: Data is incomplete!"))
Toggle("Is Funny", isOn: .constant(false))
}
}
.onAppear {
appData.nameRecord = NameRecord(name: "John")
}
}
}
I can certainly see that I'm missing something. Xcode gives errors like Value of optional type 'NameRecord?' must be unwrapped to refer to member 'name' of wrapped base type 'NameRecord') and offers some FixIts that don't help.
Based on the answer from the user "workingdog support Ukraine" I now know how to make a binding to the part I need, but the solution doesn't scale well for a record that has many fields of different type.
Given that the optional part is in the middle of appData.nameRecord.name, it seems that there might be a solution that does something like what the following function in the SwiftUI header might be doing:
public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> { get }
My SwiftFu is insufficient, so I don't know how this works, but I suspect it's what is doing the work for something like $appData.nameRecord.name when nameRecord is not an optional. I would like to have something where this function would result in a binding to .constant when anything in the keyPath is nil (or even if it did a fatalError that I would avoid with conditionals as above). It would be great if there was a way to get a solution that was as elegant as Jonathan's answer that was also suggested by workingdog for a similar situation. Any pointers in that area would be much appreciated!
Binding has a failable initializer that transforms a Binding<Value?>.
if let nameRecord = Binding($appData.nameRecord) {
TextField("Name", text: nameRecord.name)
Toggle("Is Funny", isOn: nameRecord.isFunny)
} else {
Text("Data is incomplete")
TextField("Name", text: .constant(""))
Toggle("Is Funny", isOn: .constant(false))
}
Or, with less repetition:
if appData.nameRecord == nil {
Text("Data is incomplete")
}
let bindings = Binding($appData.nameRecord).map { nameRecord in
( name: nameRecord.name,
isFunny: nameRecord.isFunny
)
} ?? (
name: .constant(""),
isFunny: .constant(false)
)
TextField("Name", text: bindings.name)
Toggle("Is Funny", isOn: bindings.isFunny)

Purchase a product using it's id or displayName

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"}) {
// ...
}

Initialize optional #AppStorage property with non-nil value

I need an optional #AppStorage String property (for a NavigationLink selection, which required optional), so I declared
#AppStorage("navItemSelected") var navItemSelected: String?
I need it to start with a default value that's non-nil, so I tried:
#AppStorage("navItemSelected") var navItemSelected: String? = "default"
but that doesn't compile.
I also tried:
init() {
if navItemSelected == nil { navItemSelected = "default" }
}
But this just overwrites the actual persisted value whenever the app starts.
Is there a way to start it with a default non-nil value and then have it persisted as normal?
Here is a simple demo of possible approach based on inline Binding (follow-up of my comment above).
Tested with Xcode 13 / iOS 15
struct DemoAppStoreNavigation: View {
static let defaultNav = "default"
#AppStorage("navItemSelected") var navItemSelected = Self.defaultNav
var body: some View {
NavigationView {
Button("Go Next") {
navItemSelected = "next"
}.background(
NavigationLink(isActive: Binding(
get: { navItemSelected != Self.defaultNav },
set: { _ in }
), destination: {
Button("Return") {
navItemSelected = Self.defaultNav
}
.onDisappear {
navItemSelected = Self.defaultNav // << for the case of `<Back`
}
}) { EmptyView() }
)
}
}
}
#AppStorage is a wrapper for UserDefaults, so you can simply register a default the old-fashioned way:
UserDefaults.standard.register(defaults: ["navItemSelected" : "default"])
You will need to call register(defaults:) before your view loads, so I’d recommend calling it in your App’s init or in application(_:didFinishLaunchingWithOptions:).

SwiftUI GKLeaderboard loadEntries

I would like to add leaderboards to my SwiftUI app.
I can't find any examples of using loadEntries to load leaderboard values.
I tried the following...
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in ...
This results in the following warnings:
'identifier' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'timeScope' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'loadScores(completionHandler:)' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler:.
using loadEntriesForPlayerScope results in the following warning:
'loadEntriesForPlayerScope(_:timeScope:range:completionHandler:)' has
been renamed to 'loadEntries(for:timeScope:range:completionHandler:)'
Using loadEntries I don't know how to specify the leaderboard identifier.
Here is simple demo of possible approach - put everything in view model and load scores on view appear.
import GameKit
class BoardModel: ObservableObject {
private var board: GKLeaderboard?
#Published var localPlayerScore: GKLeaderboard.Entry?
#Published var topScores: [GKLeaderboard.Entry]?
func load() {
if nil == board {
GKLeaderboard.loadLeaderboards(IDs: ["YOUR_LEADERBOARD_ID_HERE"]) { [weak self] (boards, error) in
self?.board = boards?.first
self?.updateScores()
}
} else {
self.updateScores()
}
}
func updateScores() {
board?.loadEntries(for: .global, timeScope: .allTime, range: NSRange(location: 1, length: 10),
completionHandler: { [weak self] (local, entries, count, error) in
DispatchQueue.main.async {
self?.localPlayerScore = local
self?.topScores = entries
}
})
}
}
struct DemoGameboardview: View {
#StateObject var vm = BoardModel()
var body: some View {
List {
ForEach(vm.topScores ?? [], id: \.self) { item in
HStack {
Text(item.player.displayName)
Spacer()
Text(item.formattedScore)
}
}
}
.onAppear {
vm.load()
}
}
}
I might be stating the obvious but have you looked at the WWDC20 videos?
Usually when there are big changes like this they cover it during WWDC that year.
Tap into Game Center: Leaderboards, Achievements, and Multiplayer
Tap into Game Center: Dashboard, Access Point, and Profile
I haven't looked at the videos but the documentation eludes that identifier might be replaced by var baseLeaderboardID: String

SwiftUI - Why can't I use an Alert within an if statement

I'm currently working through the #100daysofswiftUI and have a quick question regarding the use of alerts within if statements. This particular task is at the end of the second project.
Here is the code:
.alert(isPresented: $showingScore) {
if x == 1 {
Alert(title: Text(scoreTitle), message: Text("The correct answer was \(countries[correctAnswer])"), dismissButton: .default(Text("Restart")) {
self.askQuestion()
})
}
}
I feel like this code should work however I have a yellow alert over the Alert line saying:
Result of 'Alert' initializer is unused
I don't know what this means, how can I fix this?
You need to return an Alert in the .alert modifier, not an if statement.
For this you can create a computed property which returns an Alert:
var alert: Alert {
if x == 1 {
return Alert(
title: Text(scoreTitle),
message: Text("The correct answer was \(countries[correctAnswer])"),
dismissButton: .default(Text("Restart")) {
self.askQuestion()
}
)
} else {
return Alert(...) // return some other `Alert`
}
}
and use it in the .alert modifier:
.alert(isPresented: $showingScore) {
alert
}
Just make sure you only enable showingScore when you want to show an alert.
you should put your if condition with showingScore,
do something like this
if x == 1 {
showingScore = true
}
your alert will look like this
.alert(isPresented: $showingScore) {
Alert(title: Text(scoreTitle), message: Text("The correct answer was \(countries[correctAnswer])"), dismissButton: .default(Text("Restart")) {
self.askQuestion()
})
}