SwiftUI DidBecomeActiveNotification - all values are lost - ios

I am new to iOS, and I am developing my fairy tales application with SwiftUI. Everything is ok, but I found one error - when entering background and returning back to the app, all data gets lost from my View.
I tried to debug the app, and I see that when receiving willEnterForegroundNotification, the data still exists. But, when receiving didBecomeActiveNotification, data already gets lost.
Could anyone help me to understand, how to preserve data in the View, when entering back foreground?
import SwiftUI
import DynamicColor
struct FairyTaleStoryView: View, FairyModelDelegate, WebImageLoadProtocol {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var offset = CGSize.zero // for gesture recognition - how far the user has dragged
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
let storyId : Int
#State var pos : Int = 0
#ObservedObject var networkManager = NetworkManager()
var musicManager = MusicManager()
var states = FairyStates()
#State var textShowing = 1
.....
var storyPresenter : some View {
....
}
var body: some View {
VStack {
storyPresenter
.navigationBarTitle("")
.navigationBarHidden(true)
.onAppear(perform: {
self.networkManager.delegate = self
self.networkManager.fetchFairyTale(id: storyId)
})
}
.background(Color(hexString: Colors.BackgroundColor))
.edgesIgnoringSafeArea(.all)
.onDisappear() {
stopSound()
stopMusic()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
print(networkManager.fairyTale) // prints an object
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
print(networkManager.fairyTale) // prints empty object
}
}
}
struct FairyTaleStoryView_Previews: PreviewProvider {
static var previews: some View {
FairyTaleStoryView(storyId: 0)
}
}

Related

SwiftUI - "Argument passed to call that takes no arguments"?

I have an issue with the coding for my app, where I want to be able to scan a QR and bring it to the next page through navigation link. Right now I am able to scan a QR code and get a link but that is not a necessary function for me. Below I attached my code and got the issue "Argument passed to call that takes no arguments", any advice or help would be appreciated :)
struct QRCodeScannerExampleView: View {
#State private var isPresentingScanner = false
#State private var scannedCode: String?
var body: some View {
VStack(spacing: 10) {
if let code = scannedCode {
//error below
NavigationLink("Next page", destination: PageThree(scannedCode: code), isActive: .constant(true)).hidden()
}
Button("Scan Code") {
isPresentingScanner = true
}
Text("Scan a QR code to begin")
}
.sheet(isPresented: $isPresentingScanner) {
CodeScannerView(codeTypes: [.qr]) { response in
if case let .success(result) = response {
scannedCode = result.string
isPresentingScanner = false
}
}
}
}
}
Page Three Code
import SwiftUI
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
struct PageThree_Previews: PreviewProvider {
static var previews: some View {
PageThree()
}
}
You forgot property:
struct PageThree: View {
var scannedCode: String = "" // << here !!
var body: some View {
Text("Code: " + scannedCode)
}
}
You create your PageThree View in two ways, One with scannedCode as a parameter, one with no params.
PageThree(scannedCode: code)
PageThree()
Meanwhile, you defined your view with no initialize parameters
struct PageThree: View {
var body: some View {
Text("Hello, World!")
}
}
For your current definition, you only can use PageThree() to create your view. If you want to pass value while initializing, change your view implementation and consistently using one kind of initializing method.
struct PageThree: View {
var scannedCode: String
var body: some View {
Text(scannedCode)
}
}
or
struct PageThree: View {
private var scannedCode: String
init(code: String) {
scannedCode = code
}
var body: some View {
Text(scannedCode)
}
}
This is basic OOP, consider to learn it well before jump-in to development.
https://docs.swift.org/swift-book/LanguageGuide/Initialization.html

Importing the data typed by the user to a view?

In the process of making my first Finance App, I want the user to type their Credit Card Name and las four numbers (probably more info since this is a draft) into this Modally presented view, to then be seen in a cards index, widget-look-like.
struct CardListView: View {
#State var isPresentingAddModal = false
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
var headerView: some View {
HStack {
Text("Tus tarjetas")
Spacer()
Button("Añadir nueva") {
self.isPresentingAddModal.toggle()
}
.sheet(isPresented: $isPresentingAddModal, content: {
HStack {
Text("Emisor de tarjeta")
TextField("Seleccionar emisor de tarjeta", text: $emisorTarjeta)
}
HStack {
Text("Número de tarjeta")
TextField("Escribí tu número de tarjeta", text: $numeroTarjeta)
}
Button(action: {
self.isPresentingAddModal.toggle()
print("\(self.emisorTarjeta)")
}, label: {
Text("Añadir")
})
Spacer()
})
}
The question now is how to pass the info typed from the two textFields, to the view where the cards will be created. The button "Añadir" currently works as a dismiss button instead of an add one, since I don't know how to create that.
(Also, a lot of code like paddings and backgroundColors have been erased to make it clearer to see)
Enitre view of the homeView
Where the "añadir" button is
there are several ways to do this. One simple way is to use "#State" and "#Binding" like this:
In "CardListView" use this:
#Binding var emisorTarjeta: String
#Binding var numeroTarjeta: String
and in the "CardViewCreator" use:
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
Another way is to use "ObservableObject", create a class like this:
class CardModel: ObservableObject {
#Published var emisorTarjeta = ""
#Published var numeroTarjeta = ""
}
In the your "CardViewCreator" or some parent view:
#StateObject var cardModel = CardModel()
and pass it to the "CardListView" like this:
struct CardListView: View {
#ObservedObject var cardModel: CardModel
...
}
You can also use "EnvironmentObject" in a similar way.
It all depends on your case. I recommend reading up on "ObservedObject"
and using that.
A really simple way of doing this is to pass in a closure to run when the add button is tapped. Here's an example, which also shows how to dismiss the presented sheet
import SwiftUI
struct Card: Identifiable {
let id = UUID()
let provider: String
let number: String
}
struct ContentView: View {
#State private var cards = [Card]()
#State private var showingSheet = false
var body: some View {
VStack {
List(cards, rowContent: CardView.init)
.padding(.bottom, 10)
Button("Add") {
showingSheet = true
}
.padding()
}
.sheet(isPresented: $showingSheet) {
AddSheet(completion: addCard)
}
}
func addCard(provider: String, number: String) {
let newCard = Card(provider: provider, number: number)
cards.append(newCard)
}
}
struct CardView: View {
let card: Card
var body: some View {
VStack(alignment: .leading) {
Text(card.provider)
Text(card.number)
}
}
}
struct AddSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var provider = ""
#State private var number = ""
let completion: (String, String) -> Void
var body: some View {
VStack {
TextField("Provider", text: $provider).padding()
TextField("Number", text: $number).padding()
Button("Add") {
completion(provider, number)
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to actually save the information passed in the textfield you would have to save it somewhere and later fetch it when required But this is only if you want to be able to access the information passed into the cards index after you have closed down the application and opened it up once again.

#Published property in #EnvironmentObject doesn't update the root view of my application

I am trying to make a router object for my application using #EnvironmentObject. But the problem is the #Published property doesn't update the root view when the root view type is updated.
How it should work
A user clicks Sign in with Apple button
Update the router.currentPage property of #EnvironmentObject when a user logs in successfully.
RootView get notified for updating router.currentPage and change the root view in accordance to the updated currentPage type.
Here are my codes below.
MainApp.swift
var body: some Scene {
WindowGroup {
RootView().environmentObject(ViewRouter())
}
}
ViewRouter.swift
Code Block
enum Page {
case signin
case tasklist
}
final class ViewRouter: ObservableObject {
#Published var currentPage: Page = .signin
}
RootView.swift
struct RootView: View {
#EnvironmentObject var router: ViewRouter
var body: some View {
if router.currentPage == .signin {
SigninView()
} else {
TaskListView()
}
}
}
SigninView.swift
struct SigninView: View {
#EnvironmentObject var router: ViewRouter
#State var signInHandler: SignInWithAppleCoordinator?
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
var body: some View {
MyAppleIDButton().colorScheme(.light)
.frame(width: 280, height: 38, alignment: .center)
.onTapGesture {
signInWithAppleButtonTapped()
}
}
func signInWithAppleButtonTapped() {
guard let _window = self.window else { return }
signInHandler = SignInWithAppleCoordinator(window: _window)
signInHandler?.signIn { (user) in
router.currentPage = .tasklist
}
}
}
Update
I think I found an answer to this issue.
I created a state isLoggedIn which is checking whether or not Sign in with Apple is done successfully.
#State var isLoggedIn: Bool = false
Then I added View Modifier onChange which is checking the value change of isLoggedIn above. Inside the onChange I assigned a new value to router.currentPage like below.
.onChange(of: isLoggedIn, perform: { isLoggedIn in
if isLoggedIn {
router.currentPage = .tasklist
} else {
router.currentPage = .signin
}
})
But I am still not sure of why it doesn't work in the closure of SigninWithApple button.

MKPlacemark() creating retain cycle

I have a #State property that's initialized with a struct I created (Landmark) that takes in a MKPlacemark.
#State private var selectedLandmark: Landmark = Landmark(placemark: MKPlacemark())
Here's the code for the struct:
import Foundation
import MapKit
struct Landmark {
let placemark: MKPlacemark
var id: UUID {
UUID()
}
var name: String {
placemark.name ?? ""
}
var title: String {
placemark.title ?? ""
}
var coordinate: CLLocationCoordinate2D {
placemark.coordinate
}
}
Whenever there are any updates to any state/binding vars within this view, I get a EXC_BAD_ACCESS error and no logs. If I remove the reference to MKPlacemark, everything works fine. Is this possibly a case of a strong reference being retained somewhere?
Steps to reproduce:
Create a "child" view:
import SwiftUI
import MapKit
struct MKPlaceChild: View {
#Binding var showCreateEvent: Bool
#State private var selectedLandmark: Landmark = Landmark(placemark: MKPlacemark())
var body: some View {
VStack {
Text("Hello, World!")
Button("Cancel") {
self.showCreateEvent = false
}
}
}
}
Create a parent view:
struct MKPlaceParent: View {
#State var showCreateEvent: Bool = true
var body: some View {
VStack {
if showCreateEvent {
MKPlaceChild(showCreateEvent: $showCreateEvent)
} else {
Text("Hello")
}
}
}
}
If you tap on "Cancel" button, an error occurs :(
Any help would be appreciated
The problem is in wrong initialiser. Here is fixed part (tested with Xcode 12 / iOS 14)
struct MKPlaceChild: View {
#Binding var showCreateEvent: Bool
#State private var selectedLandmark: Landmark = Landmark(placemark:
MKPlacemark(coordinate: CLLocationCoordinate2D())) // << here !!
...

How to use Dictionary as #Binding var in SwiftUI

I will need to display a collapsed menu in SwiftUI, it is possible to pass one single bool value as binding var to subviews but got stuck when trying to pass that value from a dictionary.
see code below:
struct MenuView: View {
#EnvironmentObject var data: APIData
#State var menuCollapsed:[String: Bool] = [:]
#State var isMenuCollapsed = false;
// I am able to pass self.$isMenuCollapsed but self.$menuCollapsed[menuItem.name], why?
var body: some View {
if data.isMenuSynced {
List() {
ForEach((data.menuList?.content)!, id: \.name) { menuItem in
TopMenuRow(dataSource: menuItem, isCollapsed: self.$isMenuCollapsed)
.onTapGesture {
if menuItem.isExtendable() {
let isCollapsed = self.menuCollapsed[menuItem.name]
self.menuCollapsed.updateValue(!(isCollapsed ?? false), forKey: menuItem.name)
} else {
print("Go to link:\(menuItem.url)")
}
}
}
}
}else {
Text("Loading...")
}
}
}
in ChildMenu Row:
struct TopMenuRow: View {
var dataSource: MenuItemData
#Binding var isCollapsed: Bool
var body: some View {
ChildView(menuItemData)
if self.isCollapsed {
//display List of child data etc
}
}
}
}
If I use only one single bool as the binding var, the code is running ok, however, if I would like to use a dictionary to store each status of the array, it has the error of something else, see image blow:
if I use the line above, it's fine.
Any idea of how can I fix it?
Thanks
How to use dictionary as a storage of mutable values with State property wrapper?
As mentioned by Asperi, ForEach requires that source of data conforms to RandomAccessCollection. This requirements doesn't apply to State property wrapper!
Let see one of the possible approaches in the next snippet (copy - paste - run)
import SwiftUI
struct ContentView: View {
#State var dict = ["alfa":false, "beta":true, "gamma":false]
var body: some View {
List {
ForEach(Array(dict.keys), id: \.self) { (key) in
HStack {
Text(key)
Spacer()
Text(self.dict[key]?.description ?? "false").onTapGesture {
let v = self.dict[key] ?? false
self.dict[key] = !v
}.foregroundColor(self.dict[key] ?? false ? Color.red: Color.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the following result

Resources