unwanted response - navigationlink cycles through all pages - swift

I am doing a project where I've got some codes I am confused about. its an app to track user's habits, ContentView has a page with a list of habits, each has its own page (HabitPage) showing more detail using navigationLink.
navigationLink worked fine to show a habit page at first, but what I intend to do is the following:
user clicks a button to increase the number of times a habit is done
when the number of times its done reaches the goal, I want the app to show an alert, saying the goal's achieved
App to jump back to ContentView
I managed to do 1,2 but not 3, in my HabitPage I added an Environment var to be dismissed and a Bool to toggle. The Bool is tied to an observableobject class on a separate page. I added the Bool to navigationLink on isActive. Now what it does is that it's cycling through all the HabitPage after clicking into one from ContentView. Can someone explain the logic behind to me as I am lost to what caused it. thanks
ContentView
struct ContentView: View {
#ObservedObject var habitsCV = DataShare()
- function for deletion
var body: some View {
NavigationView {
ZStack {
//background
List {
ForEach (habitsCV.allHabitGeneral, id: \.name) { each in
**NavigationLink(destination: habitView(habitHV: self.habitsCV,singleHabit: each), isActive: self.$habitsCV.isShowingHabit) {**
- stuff for describing the habit
}
}
}
}
.onDelete(perform: itemRemove)
}
.frame(maxWidth: .infinity)
}
- buttons, another sheet which works fine and nav bar title
}
}
}
HabitPage
struct habitView : View {
**#ObservedObject var habitHV : DataShare**
var singleHabit : AllHabits
**#Environment(\.self.presentationMode) var presentationMode**
-function for the app
var body : some View {
NavigationView {
VStack(alignment: .leading) {
- text for descriptions of the habit
Button("increase timesDone") {
var o : Int = 0
- function to retrieve array index o
if self.habitHV.allHabitGeneral[o].timesDone > self.habitHV.allHabitGeneral[o].completionGoal {
-lines for alert
**self.habitHV.isShowingHabit = false**
**self.presentationMode.wrappedValue.dismiss()**
}
}
}
}
-alert
}
}
}
}
the class storing the Bool
class DataShare : ObservableObject {
- alert messages and another Boolean for a functional sheet
**#Published var isShowingHabit : Bool = false**
#Published var allHabitGeneral : [AllHabits] {
-didSet function
- initialiser

Related

NavigationLink redirecting me to previous view after appending data to Firebase Realtime Database

I am using SwiftUI and Firebase Realtime Database within my project and whenever I try changing the contents of my database, and then redirect the user to the next view, the view changes briefly and then get redirected back.
Here is a simplified version of my code to help you understand:
import SwiftUI
import Firebase
import FirebaseDatabase
struct someView: View {
private var randomText = ["hello", "world"]
#State private var showNextView = false
var ref: DatabaseReference!
init() {
ref = Database.database!.reference()
}
var body: some View {
ZStack {
NavigationLink("", destination: nextView(), isActive: $showNextView)
Button {
save()
self.showNextView.toggle()
} label : {Text("Save")}
}
}
func save() {
self.ref.child("users/\(Auth.auth().currentUser!.uid)/someName").setValue(randomText)
}
}
struct nextView: View {
var body: some View {
ZStack {
Text("This is the next view")
}
}
}
When I click the button within someView, the array is saved correctly in the database. I then get redirected to nextView where I see the text "This is the next view" - as expected. However, this only shows briefly and then the view jumps back to someView again. I am unsure why this is and cannot find any information as to how to fix it.
I assume this happens because click handled and by Button and by NavigationLink which right under button, so try to hide link completely to make it activate only programmatically, like
ZStack {
NavigationLink(destination: nextView(), isActive: $showNextView) {
EmptyView() // << here !!
}.disabled(true) // << here !!
Button {
save()
self.showNextView.toggle()
} label : {Text("Save")}
}
You need to wrap your ZStack inside a NavigationView if you want to use NavigationLink.
NavigationView {
ZStack {
NavigationLink("", destination: nextView(), isActive: $showNextView)
Button {
save()
self.showNextView.toggle()
} label : {Text("Save")}
}
}

SwiftUI: forcing focus based on state with TabView and watchOS

I'm trying to have a feature where a button enables the digital crown. There are multiple tabs with this button, and I want to be able to scroll between the tabs and keep the mode/crown enabled.
So for above, my goal is the user could touch 'Enable Crown' and be able to swipe between tabs, keeping the digital crown mode enabled.
I believe that means maintaining focus on an element that has digitalCrownRotation() and focusable().
I got this working somewhat with the code below: if the 2nd tab is not active yet and I enable the crown and swipe to tab #2, the default focus modifier triggers #2 button to get focus and get the crown input.
But after that the buttons lose focus. I've tried various tricks and can't get it working. Complete code is at https://github.com/t9mike/DigitalCrownHelp3.
import SwiftUI
class GlobalState : ObservableObject {
#Published var enableCrown = false
}
struct ChildView: View {
#State var crownValue = 0.0
#ObservedObject var globalState = ContentView.globalState
#Namespace private var namespace
#Environment(\.resetFocus) var resetFocus
let label: String
init(_ label: String) {
self.label = label
}
var body: some View {
ScrollView {
Text(label)
Button("\(globalState.enableCrown ? "Disable" : "Enable") Crown") {
globalState.enableCrown = !globalState.enableCrown
}
.focusable()
.prefersDefaultFocus(globalState.enableCrown, in: namespace)
.digitalCrownRotation($crownValue)
.onChange(of: crownValue, perform: { value in
print("crownValue is \(crownValue)")
})
}
.focusScope(namespace)
.onAppear {
print("\(label), enableCrown=\(globalState.enableCrown)")
resetFocus(in: namespace)
}
}
}
struct ContentView: View {
static let globalState = GlobalState()
var body: some View {
TabView {
ChildView("Tab #1")
ChildView("Tab #2")
}
}
}

SwiftUI: resetting TabView

I have a TabView with two tabs in a SwiftUI lifecycle app, one of them has complex view structure: NavigationView with a lot of sub-views inside, i.e.: NavigationLinks and their DestinationViews are spread on multiple levels down the view tree, each sub-view on its own is another view hierarchy with sheets and / or other DestinationViews. At some point inside this hierarchy, I want to reset the TabView to its original state which is displaying the first most view, so the user can restart their journey right at that state, as they were to open the app for the first time, so it's kinda impossible to track down something like isActive & isPresented bindings to pop-off or dismiss the views and sheets.
I thought of wrapping the TabView inside another view: RootView in an attempt to find an easy way to recreate that TabView from scratch or something like refreshing / resetting the TabView, but couldn't find a clew on how to do it.
Here's my code snippet:
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
RootView()
}
}
}
struct RootView: View {
var body: some View {
ContentView()
}
}
struct ContentView: View {
var body: some View {
TabView { // <-- I need to reset it to its original state
View1() // <---- this view has complex view hierarchy
.tabItem {
Text("Home")
}.tag(0)
View2()
.tabItem {
Text("Settings")
}.tag(1)
}
}
}
p.s. I'm not looking for "popping off the view to root view", because this can't be done when there are many active NavigationLink destinations where the user might open one of the sheets and start a new navigation journey inside the sheet.
****** UPDATE ******
I've created a new Environment value to hold a boolean that should indicate whether the TabView should reset or not, and I've tracked every isPresented and isActive state variables in every view and reset them once that environment value is set to true like this:
struct ResetTabView: EnvironmentKey {
static var defaultValue: Binding<ResetTabObservable> = .constant(ResetTabObservable())
}
extension EnvironmentValues {
var resetTabView: Binding<ResetTabObservable> {
get { self[ResetTabView.self] }
set { self[ResetTabView.self] = newValue }
}
}
class ResetTabObservable: ObservableObject {
#Published var newValue = false
}
in every view that will present a sheet or push a new view I added something like this:
struct View3: View {
#State var showSheet = false
#Environment(\.resetTabView) var reset
var body: some View {
Text("This is view 3")
Button(action: {
showSheet = true
}, label: {
Text("show view 4")
})
.sheet(isPresented: $showSheet) {
View4()
}
.onReceive(reset.$newValue.wrappedValue, perform: { val in
if val == true {
showSheet = false
}
})
}
}
and in the last view (which will reset the TabView) I toggle the Environment value like this:
struct View5: View {
#Environment(\.resetTabView) var reset
var body: some View {
VStack {
Text("This is view 5")
Button(action: {
reset.newValue.wrappedValue = true
}, label: {
Text("reset tab view")
})
}
}
}
This resulted in awkward dismissal for views:
What i do for this is i make all my presentation bindings be stored using #SceneStorage("key") (instead of #State) this way they not only respect state restoration ! but you can also access them throughout your app easily by using the same key. This post gives a good example of how this enables the switching from Tab to Sidebar view on iPad.
I used this in my apps so if i have a button or something that needs to unwind many presentations it can read on all of these values and reset them back to wanted value without needing to pass around a load of bindings.

SwiftUI - Conditional based on ObservedObject doesn't work in subsequent Views

I have a very simple app which contains two views that are tied together with a NavigationLink. In the first view, ContentView, I can see updates to my ObservedObject as expected. However, when I go to the next View, it seems that the code based on the ObservedObject does not recognize changes.
ContentView.swift (The working view):
import SwiftUI
struct ContentView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
NavigationView {
VStack(spacing: 15) {
Toggle(isOn: self.$toggleObject.isToggled) {
Text("Toggle:")
}
if self.toggleObject.isToggled == true {
Text("ON")
}
else {
Text("OFF")
}
NavigationLink(destination: ShowToggleView()) {
Text("Show Toggle Status")
}
}
}
}
}
ShowToggleView.swift (The view that does not behave as I expect it to):
import SwiftUI
struct ShowToggleView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
Form {
Section {
if self.toggleObject.isToggled {
Text("Toggled on")
}
else {
Text("Toggled off")
}
}
}
}
}
All of this data is stored in a simple file, ToggleObject.swift:
import SwiftUI
class ToggleObject: ObservableObject {
#Published var isToggled = false
}
When I toggle it on in the first View I see the text "ON" which is expected, but when I go into the next view it shows "Toggled off" no matter what I do in the first View... Why is that?
Using Xcode 11.5 and Swift 5
You are almost doing everything correct. However, you are creating another instance of ToggleObject() in your second view, which overrides the data. You basically only create one ObservedObject and then pass it to your subview, so they both access the same data.
Change it to this:
struct ShowToggleView: View {
#ObservedObject var toggleObject : ToggleObject
And then pass the object to that view in your navigation link...
NavigationLink(destination: ShowToggleView(toggleObject: self.toggleObject)) {
Text("Show Toggle Status")
}

SwiftUI: Forcing an Update

Normally, we're restricted from discussing Apple prerelease stuff, but I've already seen plenty of SwiftUI discussions, so I suspect that it's OK; just this once.
I am in the process of driving into the weeds on one of the tutorials (I do that).
I am adding a pair of buttons below the swipeable screens in the "Interfacing With UIKit" tutorial: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
These are "Next" and "Prev" buttons. When at one end or the other, the corresponding button hides. I have that working fine.
The problem that I'm having, is accessing the UIPageViewController instance represented by the PageViewController.
I have the currentPage property changing (by making the PageViewController a delegate of the UIPageViewController), but I need to force the UIPageViewController to change programmatically.
I know that I can "brute force" the display by redrawing the PageView body, reflecting a new currentPage, but I'm not exactly sure how to do that.
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
VStack {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
HStack(alignment: .center) {
Spacer()
if 0 < currentPage {
Button(action: {
self.prevPage()
}) {
Text("Prev")
}
Spacer()
}
Text(verbatim: "Page \(currentPage)")
if currentPage < viewControllers.count - 1 {
Spacer()
Button(action: {
self.nextPage()
}) {
Text("Next")
}
}
Spacer()
}
}
}
func nextPage() {
if currentPage < viewControllers.count - 1 {
currentPage += 1
}
}
func prevPage() {
if 0 < currentPage {
currentPage -= 1
}
}
}
I know the answer should be obvious, but I'm having difficulty figuring out how to programmatically refresh the VStack or body.
2021 SWIFT 1 and 2 both:
IMPORTANT THING! If you search for this hack, probably you doing something wrong! Please, read this block before you read hack solution!!!!!!!!!!
Your UI wasn't updated automatically because of you miss something
important.
Your ViewModel must be a class wrapped into ObservableObject/ObservedObject
Any field in ViewModel must be a STRUCT. NOT A CLASS!!!! Swift UI does not work with classes!
Must be used modifiers correctly (state, observable/observedObject, published, binding, etc)
If you need a class property in your View Model (for some reason) - you need to mark it as ObservableObject/Observed object and assign them into View's object !!!!!!!! inside init() of View. !!!!!!!
Sometimes is needed to use hacks. But this is really-really-really exclusive situation! In most cases this wrong way! One more time: Please, use structs instead of classes!
Your UI will be refreshed automatically if all of written above was used correctly.
Sample of correct usage:
struct SomeView : View {
#ObservedObject var model : SomeViewModel
#ObservedObject var someClassValue: MyClass
init(model: SomeViewModel) {
self.model = model
//as this is class we must do it observable and assign into view manually
self.someClassValue = model.someClassValue
}
var body: some View {
//here we can use model.someStructValue directly
// or we can use local someClassValue taken from VIEW, BUT NOT value from model
}
}
class SomeViewModel : ObservableObject {
#Published var someStructValue: Bool = false
var someClassValue: MyClass = MyClass() //myClass : ObservableObject
}
And the answer on topic question.
(hacks solutions - prefer do not use this)
Way 1: declare inside of view:
#State var updater: Bool = false
all you need to do is call updater.toggle()
Way 2: refresh from ViewModel
Works on SwiftUI 2
public class ViewModelSample : ObservableObject
func updateView(){
self.objectWillChange.send()
}
}
Way 3: refresh from ViewModel:
works on SwiftUI 1
import Combine
import SwiftUI
class ViewModelSample: ObservableObject {
private let objectWillChange = ObservableObjectPublisher()
func updateView(){
objectWillChange.send()
}
}
This is another solution what worked for me, using id() identifier. Basically, we are not really refreshing view. We are replacing the view with a new one.
import SwiftUI
struct ManualUpdatedTextField: View {
#State var name: String
var body: some View {
VStack {
TextField("", text: $name)
Text("Hello, \(name)!")
}
}
}
struct MainView: View {
#State private var name: String = "Tim"
#State private var theId = 0
var body: some View {
VStack {
Button {
name += " Cook"
theId += 1
} label: {
Text("update Text")
.padding()
.background(Color.blue)
}
ManualUpdatedTextField(name: name)
.id(theId)
}
}
}
Setting currentPage, as it is a #State, will reload the whole body.