Display sheet within init at initial app lunch - swift

I created an init like this:
init() {
// Show what's new on the update from App Store
if !UserDefaults.standard.bool(forKey: "appStoreUpdateNotification_2.2.0") {
UserDefaults.standard.set(true, forKey: "appStoreUpdateNotification_2.2.0")
}
}
How can I include the sheet there? Until now I was using a button to display it. This is to show the sheet when I update the app to inform the users of changes

You can try setting a #State variable and use it to present a sheet:
struct ContentView: View {
#State var isShowingModal: Bool = false
init() {
if !UserDefaults.standard.bool(forKey: "appStoreUpdateNotification_2.2.0") {
UserDefaults.standard.set(true, forKey: "appStoreUpdateNotification_2.2.0")
_isShowingModal = .init(initialValue: true)
}
}
var body: some View {
Text("")
.sheet(isPresented: $isShowingModal) {
SomeOtherView()
}
}
}

Related

How do I instantly load core data in a view after closing my popup?

The point of this app is to use core data to permanently add types of fruit to a list. I have two views: ContentView and SecondScreen. SecondScreen is a pop-up sheet. When I input a fruit and press 'save' in SecondScreen, I want to immediately update the list in ContentView to reflect the type of fruit that has just been added to core data as well as the other fruits which have previously been added to core data. My problem is that when I hit the 'save' button in SecondScreen, the new fruit is not immediately added to the list in ContentView. Instead, I have to restart the app to see the new fruit in the list.
Here is the class for my core data:
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntities: [FruitEntity] = []
init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error with coreData. \(error)")
}
}
fetchFruits()
}
func fetchFruits() {
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addFruit(text: String) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
saveData()
}
func saveData() {
do {
try container.viewContext.save()
fetchFruits()
} catch let error {
print("Error saving. \(error)")
}
}
}
Here is my ContentView struct:
struct ContentView: View {
//sheet variable
#State var showSheet: Bool = false
#StateObject var vm = CoreDataViewModel()
#State var refresh: Bool
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: {
showSheet.toggle()
}, label: {
Text("Add Fruit")
})
List {
ForEach(vm.savedEntities) { entity in
Text(entity.name ?? "NO NAME")
}
}
}
.navigationTitle("Fruits")
.sheet(isPresented: $showSheet, content: {
SecondScreen(refresh: $refresh)
})
}
}
}
Here is my SecondScreen struct:
struct SecondScreen: View {
#Binding var refresh: Bool
#Environment(\.presentationMode) var presentationMode
#StateObject var vm = CoreDataViewModel()
#State var textFieldText: String = ""
var body: some View {
TextField("Add fruit here...", text: $textFieldText)
.font(.headline)
.padding(.horizontal)
Button(action: {
guard !textFieldText.isEmpty else { return }
vm.addFruit(text: textFieldText)
textFieldText = ""
presentationMode.wrappedValue.dismiss()
refresh.toggle()
}, label: {
Text("Save")
})
}
}
To try and solve this issue, I've created a #State boolean variable called 'refresh' in ContentView and bound it with the 'refresh' variable in SecondScreen. This variable is toggled when the user hits the 'save' button on SecondScreen, and I was thinking that maybe this would change the #State variable in ContentView and trigger ContentView to reload, but it doesn't work.
In your second screen , change
#StateObject var vm = CoreDataViewModel()
to
#ObservedObject var vm: CoreDataViewModel
then provide for the instances that compiler will ask for
hope it helps
You need to use #FetchRequest instead of #StateObject and NSFetchRequest. #FetchRequest will call body to update the Views when the fetch result changes.

OnAppear only once the view is opened

I want to update the view with data when a view is opened so I added:
.onAppear {
loadData()
}
But I only want to update it once when the view gets opened not every time it gets reopened e.g. with a back button.
--> Only update on App start
You can put the value in UserDefaults so you will know in the second load that you have already performed loading.
extension UserDefaults {
var firstTimeVisit: Bool {
get {
return UserDefaults.standard.value(forKey: "firstTimeVisit") as? Bool ?? true
} set {
UserDefaults.standard.setValue(newValue, forKey: "firstTimeVisit")
}
}
}
struct ContentView: View {
var body: some View {
VStack {
}.task {
if UserDefaults.standard.firstTimeVisit {
// load Data
UserDefaults.standard.firstTimeVisit = false
}
}
}
}
UPDATE:
extension UserDefaults {
var firstTimeVisit: Bool {
get {
return !UserDefaults.standard.bool(forKey: "firstTimeVisit")
} set {
UserDefaults.standard.set(newValue, forKey: "firstTimeVisit")
}
}
}

SwiftUI: #Environment(\.presentationMode)'s dismiss not working in iOS14

I have a view that shows a sheet for filtering the items in a list. The view has this var:
struct JobsTab: View {
#State private var jobFilter: JobFilter = JobFilter()
var filter: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView($jobFilter, categoriesViewModel, jobsViewModel)
}
}
However, in the sheet, I'm trying the following and I can't make the view dismissed when clicking on the DONE button, only on the CANCEL button:
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var categoriesViewModel: CategoriesViewModel
#ObservedObject var jobsViewModel: JobsViewModel
let filterViewModel: FilterViewModel
#Binding var jobFilter: JobFilter
#State private var draft: JobFilter
#State var searchText = ""
init(_ jobFilter: Binding<JobFilter>, _ categoriesViewModel: CategoriesViewModel, _ jobsViewModel: JobsViewModel) {
_jobFilter = jobFilter
_draft = State(wrappedValue: jobFilter.wrappedValue)
self.categoriesViewModel = categoriesViewModel
self.jobsViewModel = jobsViewModel
self.filterViewModel = FilterViewModel()
}
...
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("FilterView.Button.Cancel.Text".capitalizedLocalization) {
presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("FilterView.Button.Done.Text".capitalizedLocalization) {
let request = Job.defaultRequest()
request.predicate = filterViewModel.buildPredicate(withJobFilterDraft: self.draft)
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Job.publicationDate), ascending: false)]
jobsViewModel.filteredJobsFetchRequest = request
self.jobFilter = self.draft
presentationMode.wrappedValue.dismiss()
}
}
}
I have also tried with a #Binding like Paul says here but there's no luck.
Is there any workaround, or am I doing something wrong?
Thanks in advance!
EDIT: I've posted the properties of both views, because I think the problem comes from the the line in FilterView self.jobFilter = self.draft.
What I'm trying to do here is create a filter view, and the aforementioned line will be executed when the user presses the DONE button: I want to assign my binding jobFilter in the JobsTab the value of the FilterView source of truth (which is a #State) and probably, since I'm updating the binding jobFilter the FilterView is being shown again even though the $showFilter is false? I don't know to be honest.
EDIT2: I have also tried
``
if #available(iOS 15.0, *) {
let _ = Self._printChanges()
} else {
// Fallback on earlier versions
}
in both `FilterView` and its called `JobTabs` and in both, I get the same result: unchanged
According to your code, I assumed your FilterView() is not a sub view, but an independent view by its own.
Therefore, to make sure "presentationMode.wrappedValue.dismiss()" works, you don't need to create #Binding or #State variables outside the FilerView() for passing the data back and forth between different views. Just create one variable inside your FilterView() to make it works.
I don't have your full code, but I created a similar situation to your problem as below code:
import SwiftUI
struct Main: View {
#State private var showFilter = false
var body: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView()
}
}
}
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
NavigationView {
VStack {
Text("Filter View")
}.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("cancel")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("okay")
}
}
}
}
}
}
the parameter onDismiss is missing:
.sheet(isPresented: $showFilter, onDismiss: { isPresented = false }) { ... }
View model objects are usually a source of bugs because we don't use those in SwiftUI instead we use structs for speed and consistency. I recommend making an #State struct containing a bool property isSheetPresented and also the data the sheet needs. Add a mutating func and call it on the struct from the button event. Pass a binding to the struct into the sheet's view where you can call another mutating function to set the bool to false. Something like this:
struct SheetConfig {
var isPresented = false
var data: [String] = []
mutating func show(initialData: [String] ) {
isPresented = true
data = initialData
}
mutating func hide() {
isPresented = false
}
}
struct ContentView: View {
#State var config = SheetConfig()
var body: some View {
Button {
config.show(intialData: ...)
} label: {
}
.sheet(isPresented: $config.isPresented) {
FilterView($config)
}
Here is what I did to handle this issue.
Create a new protocol to avoid repeatations.
public protocol UIViewControllerHostedView where Self: View {
/// A method which should be triggered whenever dismiss is needed.
/// - Note: Since `presentationMode & isPresented` are not working for presented UIHostingControllers on lower iOS versions than 15. You must call, this method whenever you want to dismiss the presented SwiftUI.
func dismissHostedView(presentationMode: Binding<PresentationMode>)
}
public extension UIViewControllerHostedView {
func dismissHostedView(presentationMode: Binding<PresentationMode>) {
// Note: presentationMode & isPresented are not working for presented UIHostingControllers on lower iOS versions than 15.
if #available(iOS 15, *) {
presentationMode.wrappedValue.dismiss()
} else {
self.topViewController?.dismisOrPopBack()
}
}
}
Extend UIWindow and UIApplication
import UIKit
public extension UIWindow {
// Credits: - https://gist.github.com/matteodanelli/b8dcdfef39e3417ec7116a2830ff67cf
func visibleViewController() -> UIViewController? {
if let rootViewController: UIViewController = self.rootViewController {
return UIWindow.getVisibleViewControllerFrom(vc: rootViewController)
}
return nil
}
class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
switch(vc){
case is UINavigationController:
let navigationController = vc as! UINavigationController
return UIWindow.getVisibleViewControllerFrom( vc: navigationController.visibleViewController!)
case is UITabBarController:
let tabBarController = vc as! UITabBarController
return UIWindow.getVisibleViewControllerFrom(vc: tabBarController.selectedViewController!)
default:
if let presentedViewController = vc.presentedViewController {
if let presentedViewController2 = presentedViewController.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(vc: presentedViewController2)
}
else{
return vc;
}
}
else{
return vc;
}
}
}
}
#objc public extension UIApplication {
/// LCUIComponents: Returns the current visible top most window of the app.
var topWindow: UIWindow? {
return windows.first(where: { $0.isKeyWindow })
}
var topViewController: UIViewController? {
return topWindow?.visibleViewController()
}
}
Extend View to handle UIHostingController presented View`
public extension View {
weak var topViewController: UIViewController? {
UIApplication.shared.topViewController
}
}
Finally, use the helper method in your SwiftUI view as follows: -
struct YourView: View, UIViewControllerHostedView {
// MARK: - Properties
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body some View {
Button {
// Note: this part trigger a method in a created protocol above.
dismissHostedView(presentationMode: presentationMode)
} label: {
Text("Tap to dismiss")
}
}
In the sheet try adding this instead.
#Environment(\.dismiss) var dismiss // EnvironmentValue
Button("Some text") {
// Code
dismiss()
}

SwiftUI: Reinitialize after popover.show

I am writing a MacOS (10.15 Catalina) application using a popover. The main ContentView includes a custom view with a simple toggle:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover=NSPopover()
func applicationDidFinishLaunching(_ aNotification: Notification) {
self.popover.contentViewController = NSHostingController(rootView: contentView)
self.statusBarItem = NSStatusBar.system.statusItem(withLength: 18)
if let statusBarButton = self.statusBarItem.button {
statusBarButton.title = "☰"
statusBarButton.action = #selector(togglePopover(_:))
}
func show() {
let statusBarButton=self.statusBarItem.button!
self.popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
func hide() {
popover.performClose(nil)
}
#objc func togglePopover(_ sender: AnyObject?) {
self.popover.isShown ? hide() : show()
}
}
struct ContentView: View {
var body: some View {
Test("Hello")
// more stuff
}
}
struct Test: View {
var message: String
#State private var clicked: Bool = false
init(message: String) {
self.message = message
_clicked = State(initialValue: false)
print("init")
}
var body: some View {
return HStack {
Text(message)
Button("Click") {
self.clicked = true
}
if !self.clicked {
Text("Before")
}
else {
Text("After")
}
}
}
}
I would like to reinitialize some data in custom view whenever the popup reappears. So, in this example, clicked should reset to false. I have tried every combination of #Binding and #State variables I could find in my many searches, but nothing appears to work. It appears that .onAppear() only fires the first time.
The init() function is there because in my application I also need to include additional content and code. In this example, I have tried to use it to initialize the clicked state variable, but, though the print() function does print, the variable doesn’t seem to get reset.
How can I reinitialize the #State variable?
To initialize state in init you should not initialize it as property (because properties are initialised before init and state is initialised only once), so
struct Test: View {
var message: String
#State private var clicked: Bool // << here, only declare
init(message: String) {
self.message = message
_clicked = State(initialValue: false) // << then this works
print("init")
}
// ... other code
}

Why is part of previous SwiftUI view showing on newly created view

My first initial view for my app is a splash screen:
This screen has code to check if an authToken is available, if it's not then it will toggle the showLogin bool, which in turn causes my view to go to the LoginView(). Else if there is a authToken available, then it calls a function which fetches information about the user, stores it in an Observable Object and then sets showMain bool to true, which in turn changes my view to my main screen.
It works, but whenever it changes view, it puts the previous view at the bottom of the new view. I know I should use a navigation link for this but I can't get it to work properly with my logic and this is the closest I've gotten:
This is my code:
struct Splash: View {
let keychain = KeychainSwift()
#EnvironmentObject var user: userData
#State var showLogin = false
#State var showMain = false
var body: some View {
NavigationView{
VStack{
if(showLogin){
LoginView()
}
if(showMain){
Main()
}
Text("Splash Screen")
.onAppear(){
if(checkLogin()){
let authToken = keychain.get("authToken")
getDataAPI().getUserData(authToken: authToken!, completion: { response, error in
print("Starting")
if(error == nil){
let code = response!["code"] as? String
if(code == "100"){
print("Done")
DispatchQueue.main.async {
user.email = response?["email"] as! String
user.uid = response?["uid"] as! String
user.username = response?["username"] as! String
}
showMain.toggle()
}else{
print(error!)
}
}
})
}else{
showLogin.toggle()
}
}
}
}
}
}
Because they all in VStack, ie. one below another. To solve this you have to remove splash view from view hierarchy, explicitly, like
VStack{
if(showLogin){
LoginView()
}
if(showMain){
Main()
}
if !showLogin && !showMain {
Text("Splash Screen")
// ... other your code
}
}