iOS 15 NavigationLink pop automatically if destination is created dynamically - swiftui

This is a problem on iOS 15 RC only. It works all good on iOS 14.
The problem is if my destination on NavigationLink is created dynamically, it pop automatically if i try push a view via NavigationLink. Here is a bit code snippet
NavigationLink(
destination: (createSettingNavigationLink(name: nameAndImage[0])),
label: {
CellButton(title: nameAndImage[0], image: nameAndImage[1])
}
func createSettingNavigationLink(name: String) -> some View {
if name == "shop.Settings" {
return ShopSettings()
}
if name == "shop.Goods" {
return ShopGoodsManagementPage()
}
if name == "shop.Order" {
return ShopOrderPage()
}
if name == "Customer Service" {
return ServiceListPage(isStore: true)
}
if name == "shop.Performance" {
return PerformanceManagementPage()
}
if name == "shop.Earnings" {
return CommissionSummaryPage()
}
if name == "shop.Increase_sales" {
return BCWebView(urlStr: Constants.increaseSalesGuide)
.navigationBarTitle(Text("Increase Sales Guide"), displayMode: .inline)
.navigationBarHidden(false)
)
}
return EmptyView()
}
)
By the way, the approach mentioned here SwiftUI Unexpectedly NavigationLink pops automatically does not help.

My final solution is to change the above function to struct view. I think it makes sense since the struct view seems to keep states while function is pure stateless.

Related

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

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

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

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:).

Assign a string value to a toggle switch in swiftui

i have a form that works correctly but would like to add a toggle to indicated if a garment is used or new. if it is new toggle on, if its used a condtion box will show where the user can input the garments condition.
when then toggle is on, i would like to set the value to static string value of "New"
code i have tried:
Form{
VStack {
Toggle(isOn: self.$itemNew) {
Text("is the item new?")
self.$Cond == "New"
}
if !itemNew {
TextField("Item Condition:", text: self.$Cond) {}
}
}
}
that code didnt seem to work, any pointers on where i should be looking to solve this one?
Thanks
You don't have to use a $, you can create a Binding manually like this:
Toggle(isOn: Binding(get: {self.cond == "New" ? true : false},
set: {
if newValue == true{
self.cond == "New"
}else{
self.cond == "Not really new"
}
})) {
Text("is the item new?")
}
And the self.cond is still must be a #State variable to trigger View update its visual presentation.
But your case isn't look very ordinary for that instrument. Is there other possible values of the string?

Check state of Option-Key in SwiftUI (macOS)

I'm looking for a way to check the state of the option-key in SwiftUI on macOS.
I.e. depending on whether the option key is pressed or not I want to perform different actions in the .onTapGesture closure.
macOS-only SwiftUI has .modifiers modifier to specify EventModifiers, so your case is covered like in below example:
Rectangle()
.fill(Color.yellow)
.frame(width: 100, height: 40)
.gesture(TapGesture().modifiers(.option).onEnded {
print("Do anyting on OPTION+CLICK")
})
.onTapGesture {
print("Do anyting on CLICK")
}
While using the modifiers method on the gesture should probably be preferred, one can also actually test for the option key itself using CGEventSource in CoreGraphics:
import CoreGraphics
extension CGKeyCode
{
static let kVK_Option : CGKeyCode = 0x3A
static let kVK_RightOption: CGKeyCode = 0x3D
var isPressed: Bool {
CGEventSource.keyState(.combinedSessionState, key: self)
}
static var optionKeyPressed: Bool {
return Self.kVK_Option.isPressed || Self.kVK_RightOption.isPressed
}
}
This lets you detect the option key (or any other keys for that matter) in contexts where there isn't a modifier property or method.
The key codes in the extension can be renamed to be more Swifty, but those are the names that go way back to Classic MacOS's Toolbox and were defined in Inside Macintosh. I have a gist containing all the old key codes.
2022
.onTapGesture {
if NSEvent.modifierFlags.contains(.option) {
print("Option key Tap")
} else {
print("Tap")
}
}
.onDrag {
if NSEvent.modifierFlags.contains(.option) {
return NSItemProvider(object: "\(item.id)" as NSString)
}
return NSItemProvider()
}