SwiftUI - Showing and hiding a custom alert - ios

I am creating a custom alert reusable modifier which will have different number of buttons depending upon the style I need. Anyways in my sample code below, I have removed as much extra code as I can to keep it small and simple.
With the code below, I am able to show the alert, but clicking on "Click Me" button does not dismiss the alert. I feel something is wrong with the way I am setting "isPresented" binding variable, but not able to figure out what. Am new to SwiftUI.
Custom Alert Modifier:
struct CustomAlertView: ViewModifier {
var isPresented: Binding<Bool>
init(isPresented: Binding<Bool>) {
self.isPresented = isPresented
}
func body(content: Content) -> some View {
content.overlay(alertContent())
}
#ViewBuilder
private func alertContent() -> some View {
GeometryReader { geometry in
if self.isPresented {
// Do something
// Setting isPresented to false is not doing anything when button is clicked.
Button(action: { self.isPresented.wrappedValue = false }) {
Text("Click Me")
}
}
}
}
}
func customAlert(isPresented: Binding<Bool>) -> some View {
return modifier(CustomAlertView(isPresented: isPresented))
}
View and view model code:
struct MyView: View {
#ObservedObject var viewModel: CustomViewModel = CustomViewModel()
var body: some View {
VStack {
switch viewModel.state {
case .idle:
Color.clear.onAppear(perform: { viewModel.doSomething() })
case .showNewView:
// Navigate
}
}.onAppear() {
viewModel.state = .idle
}.customAlert(isPresented: .constant(viewModel.showAlert))
}
}
#MainActor
class CustomViewModel: ObservableObject {
enum State {
case idle
case showNewView
}
#Published var state = State.idle
#Published var showAlert = false
func doSomething() {
if failure {
self.showAlert = true
} else {
self.state = .showNewView
}
}
}
What am I doing wrong or why am I not able to dismiss the alert?
Thanks for looking!

First of all, you want #Binding var isPresented: Bool inside the CustomAlertView, and you assign it in init as self._isPresented = isPresented:
struct CustomAlertView: ViewModifier {
#Binding var isPresented: Bool // <-- like this
init(isPresented: Binding<Bool>) {
self._isPresented = isPresented // <-- and like this
}
//...
And second, you definitely don't want to set isPresented to a constant in .customAlert(isPresented: .constant(viewModel.showAlert)). Instead, you need to pass the binding variable:
VStack {
// ...
}
.customAlert(isPresented: $viewModel.showAlert) // <-- bind it here

Related

Is there a workaround for using .scenePhase with SignInWithAppleButton in a NavigationStack causing a nav stack pop and login to fail?

I converted my app to use the new NavigationStack in my onboarding flow. My login view that uses SignInWithAppleButton failed. What seemed to happen is my view containing the button popped away when Apple's sign-in button was shown. This meant my handlers were gone, so none of my handlers were called. The same code outside the NavigationStack works fine.
I eventually tracked this down to a very unexpected statement.
import SwiftUI
#main
struct TestNavSignInApp: App {
// When scenePhase isn't commented out the SignInWithAppleButton view pops off
// and the sign-in never occurs. Comment it out, and everything works fine.
#Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#ObservedObject var onboarding = OnboardingFlow()
var body: some View {
NavigationStack(path: $onboarding.path) {
Button("Start") {
onboarding.start()
}
.navigationDestination(for: OnboardDestination.self) { destination in
ViewFactory.viewForDestination(destination)
}
}
.environmentObject(onboarding)
}
}
enum OnboardDestination {
case welcomPage
case loginPage
}
class ViewFactory {
#ViewBuilder
static func viewForDestination(_ destination: OnboardDestination) -> some View {
switch destination {
case .welcomPage:
WelcomeView()
case .loginPage:
SignInWithAppleView()
}
}
}
class OnboardingFlow: ObservableObject {
#Published var path = NavigationPath()
func gotoHomePage() {
path.removeLast(path.count)
}
func gotoPrev() {
path.removeLast()
}
func start() {
path.append(OnboardDestination.welcomPage)
}
func next() {
path.append(OnboardDestination.loginPage)
}
}
struct WelcomeView: View {
#EnvironmentObject var onboarding: OnboardingFlow
var body: some View {
Button("Next") {
onboarding.next()
}
.navigationBarBackButtonHidden(true)
}
}
struct SignInWithAppleView: View {
#EnvironmentObject var onboarding: OnboardingFlow
var body: some View {
VStack(spacing: 18) {
SignInWithAppleButton(
.signIn,
onRequest: configure,
onCompletion: handle
)
}
.navigationBarBackButtonHidden(true)
}
func configure(_ request: ASAuthorizationAppleIDRequest) {
request.requestedScopes = [.email, .fullName]
}
func handle(_ authResult: Result<ASAuthorization, Error>) {
print("Handle:")
switch authResult {
case.success(let auth):
print("Authorization successful: \(auth)")
switch auth.credential {
case let credential as ASAuthorizationAppleIDCredential:
print("credential: \(credential)")
default:
print("Authorization default credential: \(auth.credential.description)")
}
case.failure(let error):
print("Authorization failed:error: \(error.localizedDescription)")
}
}
}
I'm not sure why this would happen. While scenePhase isn't a must have, I was wondering if anyone could explain what is happening and a possible workaround?

SwiftUI: #State variable never get updated from #Published

I'm trying to trigger an alert when is an error in the model but it never get updated to show the alert:
Here is my implementation in the view:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
#State var showAlert = false
init() {
viewModel = ViewModel()
showAlert = viewModel.showAlert
}
var body: some View {
NavigationView {
Text("Hello, world!")
.padding()
}
.alert(isPresented: $showAlert) {
Alert(title: Text("This works"),
message: Text("Hello"),
dismissButton: .default(Text("got it"))
)}
}
}
Here is my models:
class ViewModel: ObservableObject {
#Published var showAlert = false
var cancellables = Set<AnyCancellable>()
init() {
DoSomething.shared.showAlert.sink { _ in
print("got new Value")
} receiveValue: {[weak self] value in
print("value")
self?.showAlert = value
}.store(in: &cancellables)
}
}
class DoSomething {
let showAlert = PassthroughSubject<Bool, Never>()
static let shared = DoSomething()
private init() {
checkToShowAlert()
}
func checkToShowAlert() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
print("change value")
self?.showAlert.send(true)
}
}
}
Any of you knows why the showAlert variable it never gets updated?
I'll really appreciate your help
In your current code, you're setting ContentView's showAlert to the ViewModel's showAlert at that point in time:
init() {
viewModel = ViewModel()
showAlert = viewModel.showAlert //<-- assignment at the time of init
}
Meaning, it's false at the time of assignment. Because it's just a Bool getting assigned to another Bool, there's no mechanism to keep it updated if ViewModel's showAlert changes.
The simplest solution is to get rid of your #State variable and observe the #Published property directly:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel()
var body: some View {
NavigationView {
Text("Hello, world!")
.padding()
}
.alert(isPresented: $viewModel.showAlert) {
Alert(title: Text("This works"),
message: Text("Hello"),
dismissButton: .default(Text("got it"))
)}
}
}
Best remove the view model object, we don't need those in SwiftUI because the View struct holds the view data and the #State and #Binding property wrappers make the struct behave like an object.
Also, I don't think you need Combine for what you are trying to do because you aren't combining anything using combineLatest etc, but when we do use it in SwiftUI we don't use sink or store, instead we assign the end of the pipeline to an #Published.
Example:
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
VStack{
Text("Hello, world!")
.padding()
Button("Show Alert") {
showAlert()
}
}
}
.alert(isPresented: $isPresented) {
Alert(title:Text("This works"),
message: Text("Hello"),
dismissButton: .default(Text("got it"))
)}
}
func showAlert() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
isPresented = true
}
}
}

How to use if/ForEach in a SwiftUI View to show IAP modal?

I'm trying to display a Subscribe Now modal view instantly after the app starts to encourage users to subscribe to the Pro In-App Purchase, so I used the .onAppear modifier, and it works fine only if I want to show the modal every time the app starts.
struct ContentView: View {
#State private var selection: String? = nil
#State private var showModal = false
#ObservedObject var storeManager: StoreManager
var body: some View {
NavigationView {
VStack {
// Contents Here
}
}
.onAppear {
self.selection = "Pro"
self.showModal.toggle()
}
.sheet(isPresented: $showModal) {
if self.selection == "Pro" {
Pro(showModal: self.$showModal, storeManager: self.storeManager)
.onAppear(perform: {
SKPaymentQueue.default().add(storeManager)
})
}
}
}
}
Now, the problem begins when I want to display the modal only to those who have not subscribed yet to the Pro IAP, so I modified .onAppear to:
.onAppear {
ForEach(storeManager.myProducts, id: \.self) { product in
VStack {
if !UserDefaults.standard.bool(forKey: product.productIdentifier) {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
}
But, the if and ForEach seems not to work smoothly with structs and views. How should I use them in my case?
Update:
Based on the answers, I have changed the loop inside .onAppear to make the code conforms to SwiftUI requirements:
.onAppear {
storeManager.myProducts.forEach { product in
// Alternatively, I can use (for in) loop:
// for product in storeManager.myProducts {
if !UserDefaults.standard.bool(forKey: product.productIdentifier) {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
Now, errors have gone away but the modal is not displayed on startup.
I discovered that the problem is, storeManager.myProducts is not loaded in .onAppear modifier, while it's loaded correctly when I put the same loop in a button instead of .onAppear, any ideas? Why does onAppear doesn't load the IAP? Where should I put the code to make the modal run when the view loaded?
Update 2:
Here is a Minimal Reproducible Example:
App:
import SwiftUI
#main
struct Reprod_SOFApp: App {
#StateObject var storeManager = StoreManager()
let productIDs = ["xxxxxxxxxxxxxxxxxxxxx"]
var body: some Scene {
DocumentGroup(newDocument: Reprod_SOFDocument()) { file in
ContentView(document: file.$document, storeManager: storeManager)
.onAppear() {
storeManager.getProducts(productIDs: productIDs)
}
}
}
}
ContentView:
import SwiftUI
import StoreKit
struct ContentView: View {
#Binding var document: Reprod_SOFDocument
#State private var selection: String? = nil
#State private var showModal = false
#ObservedObject var storeManager: StoreManager
var test = ["t"]
var body: some View {
TextEditor(text: $document.text)
.onAppear {
// storeManager.myProducts.forEach(id: \.self) { product in
// Alternatively, I can use (for in) loop:
for i in test {
if !i.isEmpty {
self.selection = "Pro"
self.showModal.toggle()
}
}
}
.sheet(isPresented: $showModal) {
if self.selection == "Pro" {
Modal(showModal: self.$showModal, storeManager: self.storeManager)
.onAppear(perform: {
SKPaymentQueue.default().add(storeManager)
})
}
}
}
}
Modal:
import SwiftUI
import StoreKit
struct Modal: View {
#Binding var showModal: Bool
#ObservedObject var storeManager: StoreManager
var body: some View {
Text("hello world")
}
}
StoreManager:
import Foundation
import StoreKit
class StoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
#Published var myProducts = [SKProduct]()
var request: SKProductsRequest!
#Published var transactionState: SKPaymentTransactionState?
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
transactionState = .purchasing
case .purchased:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
transactionState = .purchased
case .restored:
UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
queue.finishTransaction(transaction)
transactionState = .restored
case .failed, .deferred:
print("Payment Queue Error: \(String(describing: transaction.error))")
queue.finishTransaction(transaction)
transactionState = .failed
default:
queue.finishTransaction(transaction)
}
}
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
print("Did receive response")
if !response.products.isEmpty {
for fetchedProduct in response.products {
DispatchQueue.main.async {
self.myProducts.append(fetchedProduct)
}
}
}
for invalidIdentifier in response.invalidProductIdentifiers {
print("Invalid identifiers found: \(invalidIdentifier)")
}
}
func getProducts(productIDs: [String]) {
print("Start requesting products ...")
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Request did fail: \(error)")
}
func purchaseProduct(product: SKProduct) {
if SKPaymentQueue.canMakePayments() {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
} else {
print("User can't make payment.")
}
}
func restoreProducts() {
print("Restoring products ...")
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
Here is a link to Minimal Reproducible Example
Instead of using .onAppear modifier to display the modal, you can change the initial values of selection and showModal:
#State private var selection: String? = "Pro"
#State private var showModal = !UserDefaults.standard.bool(forKey: "xxxxxxxxxxxxxxxxxxxxx") ? true : false
// Write your product identifier instead of "xxxxxxxxxxxxxxxxxxxxx"
This way, modal view will be shown instantly after the content view loads.
Note: For showModal, I've applied a conditional if instead of simply true, since you said you want to show the modal only to those who have not subscribed yet to the Pro IAP.
I would recommend to separate logic into a viewmodel, and you only need to manage one identified object to show your pro modal.
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
Text("Hello")
.onAppear(perform: viewModel.fetchStatus)
.sheet(item: $viewModel.carrier) { carrier in
ModalView(storeManager: carrier.storeManager)
}
}
}
class ContentViewModel: ObservableObject {
#Published var carrier: ModalObject?
private let storeManager: StoreManager
func fetchStatus() {
// do something asynchronous like
storeManager.fetchProducts() { [self] products in
if !products.contains(proProducts) {
self.carrier = ModalObject(storeManager: self.storeManager)
}
}
}
}
struct ModalObject: Identifiable {
var id = UUID()
let storeManager: StoreManager
}
I just wrote without compiling, please check with your xcode.
Your .onAppear{} should be Swift code instead SwiftUI (ForEach, VStack). VStack are view structs.

Running code when SwiftUI Toggle Changes Value

The general structure of my code is that I have a UIKit app in which I am trying to embed a swiftui view. So I have a file called SettingsViewController which is as follows:
class SettingsViewController: UIViewController {
...
var items: [(SettingsView.Setting)] = ...
var actionsForItems: [() -> Void = []
#State var isOn: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
actionsForItems = ...
...
addSettingCell(isOn: isOn)
let childView = UIHostingController(rootView: SettingsView(settings: items, actionsForSettings: actionsForItems))
addChild(childView)
childView.view.frame = container.bounds
container.addSubview(childView.view)
childView.didMove(toParent: self)
}
...
func addCell(isOn: Bool) {
items.insert((settingName, $isOn as AnyObject) as SettingsView.Setting)
actionsForItems.insert({
self.onSwitchValueChanged(isOn: isOn) //defined later
})
}
}
which creates a view called Settings View which is structured as follows:
struct SettingsView: View {
typealias Setting = (key: String, value: AnyObject?)
var settings: [Setting]
var actions: [() -> Void]
var body: some View {
List(settings.indices) { index in
...
SettingsWithSwitchView(setting: settings[index], action: actions[index], isOn: setting.value as Binding<Bool>)
}
Spacer()
}
}
and SettingsWithSwitchView is as follows:
struct SettingsWithSwitchView: View {
var setting: SettingsView.Setting
var action: () -> Void
#Binding var isOn: Bool {
willSet {
print("willSet: newValue =\(newValue) oldValue =\(isOn)")
}
didSet {
print("didSet: oldValue=\(oldValue) newValue=\(isOn)")
action()
}
}
var body: some View {
HStack {
Text(setting.key)
.foregroundColor(Color("GrayText"))
.font(Font.custom("OpenSans", size: 15))
Spacer()
Toggle(isOn: $isOn) {}
}
}
}
I read in another post here on Stack Overflow that calling didSet on the isOn property should be the way to accomplish this, but I need to call onSwitchValueChanged when the Toggle value is updated, but my current setup does not work. I would really appreciate some help to figure out how to do this. I can update with some other information if necessary.
The thing that ended up working for me was creating a ViewModel which was also an ObservableObject and then setting the action for the toggle inside of .onTapGesture

When I use withAnimation in the Model, no animation happens, why?

here is my ViewModel
import SwiftUI
class SetGameVM: ObservableObject {
#Published var model: SetGame = SetGame()
var cards: Array<SetGame.Card> {
model.cards
}
func selectCard(card: SetGame.Card) {
model.selectCard(card: card)
}
func dealCards() {
model.dealMoreCards()
}
func reset() {
model = SetGame()
}
}
and Model
import SwiftUI
struct SetGame {
var cards: Array<Card>
mutating func selectCard(card: Card) -> Bool {
...
withAnimation {
dealMoreCards()
}
...
}
}
If I use withAnimation in ViewModle or View then animation happens as expected, else if I use withAnimation in the Model which wraps some changes, View still reflect those changes, but no animation happens.
Do it in view model
func selectCard(card: SetGame.Card) {
withAnimation { // << here !!
model.selectCard(card: card)
}
}

Resources