SwiftUI doubly nested NavigationLink views not responding to changing ObservedObject - swiftui-navigationlink

I've created three views. I'm passing ObservedObject state to each view in sequence. When I change the state in the last view (AnotherView2), my application does not show the Text view with 'YES IT IS FINISHED!'. However, if I uncomment this line in AnotherView
self.userDefaultsManager.setupComplete = true
it works as I expected by showing the text.
struct ContentView: View {
#ObservedObject var userDefaultsManager = UserDefaultsManager()
#State var showAnotherView: Bool = false
var body: some View {
NavigationView {
VStack {
if !userDefaultsManager.setupComplete {
Button(action: {
self.showAnotherView.toggle()
}) {
Text("Show Another View")
}
NavigationLink(destination: AnotherView(userDefaultsManager: userDefaultsManager), isActive: $showAnotherView, label: {
EmptyView()
})
} else {
Text("YES IT IS FINISHED!")
}
}
}
}
}
struct AnotherView: View {
#ObservedObject var userDefaultsManager: UserDefaultsManager
#State var showAnotherView2: Bool = false
var body: some View {
VStack {
Button(action: {
//self.userDefaultsManager.setupComplete = true
self.showAnotherView2 = true
}, label: {
Text("Press")
})
NavigationLink(destination: AnotherView2(userDefaultsManager: userDefaultsManager), isActive: $showAnotherView2, label: {
EmptyView()
})
}
}
}
struct AnotherView2: View {
#ObservedObject var userDefaultsManager: UserDefaultsManager
var body: some View {
Button(action: {
self.userDefaultsManager.setupComplete = true
}, label: {
Text("Just Do It")
})
}
}
class UserDefaultsManager: ObservableObject {
#Published var setupComplete: Bool = UserDefaults.standard.bool(forKey: "setupComplete") {
didSet { UserDefaults.standard.set(self.setupComplete, forKey: "setupComplete") }
}
}
Can someone help me understand what is wrong with my code or the API that it won't work on double nested calls to show views in this manner?
EDIT: Using XCode 11.3 & iOS 13.3.1

I suppose here is a layout that gives required behaviour
var body: some View {
Group {
if !userDefaultsManager.setupComplete {
NavigationView {
VStack {
Button(action: {
self.showAnotherView.toggle()
}) {
Text("Show Another View")
}
NavigationLink(destination: AnotherView(userDefaultsManager: userDefaultsManager), isActive: $showAnotherView, label: {
EmptyView()
})
}
}
} else {
Text("YES IT IS FINISHED!")
}
}
}

Related

iOS16 - How to navigate through hidden links with an optional value?

I previously used an optional value to click a hidden link to navigate in my app. Something like this example in Hacking With Swift:
#State private var selection: String? = nil
var body: some View {
....
NavigationLink(destination: Text("View A"), tag: "A", selection: $selection) { EmptyView() }
NavigationLink(destination: Text("View B"), tag: "B", selection: $selection) { EmptyView() }
Button("Tap to show A") {
selection = "A"
}
Button("Tap to show B") {
selection = "B"
}
With iOS 16 this is deprecated. I am currently setting an optional value and when it's not nil I want a link to open. I can't figure out how to do it with the new NavigationLink/Value/Destination combination. Has anyone else figured out how to do it?
I created a new projects and switched ContentView to the following:
private enum Destinations: Hashable {
case empty
case general
case myQuestionView(String)
}
struct ContentView: View {
#State private var selection: Destinations?
#State private var mySelectedString: String?
var body: some View {
NavigationSplitView {
List(selection: $selection) {
NavigationLink(value: Destinations.general) {
Text("Example")
}
if mySelectedString != nil {
NavigationLink(value: Destinations.myQuestionView(mySelectedString!)) {
Text("String: \(mySelectedString ?? "no name")")
}
}
Button(action: {
mySelectedString = "A Name"
}, label: {
Text("Set the value")
})
}
} detail: {
NavigationStack {
switch selection ?? .empty {
case .empty: Text("Please select an option to continue.")
case .general: Text("Result of this option")
case .myQuestionView(let aString): Text("Hello \(aString)")
}
}
}
}
}
Here the Set the value button sets the selectedString which makes the link appear but I can't make it automatically navigate AND, ideally, it would never appear and would navigate when the value is set.
Okay... the answer is super simple and leaving the question and this answer up in case it helps someone else.
You don't need to have an invisible NavigationLink. You just need to set the selection to the new destination: selection = .myQuestionView("this name instead"). Or, if I was using the navigation path NavigationStack(path: $myPath) I'd just append it.
So in this specific example ContentView would now be:
private enum Destinations: Hashable {
case empty
case general
case myQuestionView(String)
}
struct ContentView: View {
#State private var selection: Destinations?
var body: some View {
NavigationSplitView {
List(selection: $selection) {
NavigationLink(value: Destinations.general) {
Text("Example")
}
Button(action: {
selection = .myQuestionView("this name instead")
}, label: {
Text("Set the value")
})
}
} detail: {
NavigationStack {
switch selection ?? .empty {
case .empty: Text("Please select an option to continue.")
case .general: Text("Result of this option")
case .myQuestionView(let aString): Text("Hello \(aString)")
}
}
}
}
}

Better way to raise number pad SwiftUI

Upon navigating to a view, I want the number pad to already be raised. Right now I have a solution that works the first time (albeit with a delay) but fails to raise the number pad if the user navigates back a second time. Is there a better way to raise the number pad in SwiftUI (or to have it always up)?
Example Code:
struct ParentView: View {
#FocusState var numberPadFocused: Bool
#State var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
Button {
numberPadFocused = true
isActive = true
print("Called")
} label: {
Text("Navigate")
}
NavigationLink(destination: ChildView(focusState: $numberPadFocused), isActive: $isActive) { Color.white }
}
}
}
}
struct ChildView: View {
#State var text: String = ""
#FocusState.Binding var focusState: Bool
var body: some View {
TextField("Enter Number...", text: $text)
.keyboardType(.numberPad)
.focused($focusState)
}
}

SwiftUI NavigationView, going back if NavigationLink is inside a NavigationBarItem

As there are some problems with iOS 13.4 and Xcode 11.4 with presentationMode.wrappedValue.dismiss() I am looking for an alternative approach to go back programmatically. I found this solution from MScottWaller:
iOS SwiftUI: pop or dismiss view programmatically
Unfortunately, in my case it does not work:
struct MasterView: View {
#State private var showDetail = false
var body: some View {
VStack {
Text("MasterView")
.navigationBarItems(trailing: HStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Image(systemName: "tag")
.padding(.leading, 4)
}
})
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
If the NavigationLink is inside a navigationBarItem, I can't go back from my DetailView. I don't know if it is a bug or there are other reasons why NavigationLink does not work in the same way inside a navigationBarItem.
As a workaround I use this variant with a empty NavigationLink inside the view. It works, but I don't like this:
struct MasterView: View {
#State private var showDetail = false
var body: some View {
VStack {
Text("MasterView")
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
EmptyView()
}
.navigationBarItems(trailing: HStack {
Button(action: { self.showDetail.toggle() }) {
Image(systemName: "tag")
.padding(.leading, 4)
}
})
}
}
}
Any ideas why the NavigationLink does not correct work inside a navigationBarItem?
It's an iOS bug.
https://forums.developer.apple.com/thread/125937
The work around is to toggle a NavigationLink hidden outside of nav bar:
struct Parent: View {
#State private var showingChildView = false
var body: some View {
NavigationView {
VStack {
Text("Hello World")
NavigationLink(destination: Child(),
isActive: self.$showingChildView)
{ Text("HiddenLink").hidden() }
}
.navigationBarItems(trailing: Button(action:{ self.showingChildView = true }) { Text("Next") })
}
}
}

Load more functionality using SwiftUI

i have used ScrollView with HStack, now i need to load more data when user reached scrolling at last.
var items: [Landmark]
i have used array of items which i am appeding in HStack using ForEach
ScrollView(showsHorizontalIndicator: false) {
HStack(alignment: .top, spacing: 0) {
ForEach(self.items) { landmark in
CategoryItem(landmark: landmark)
}
}
}
What is the best possible solution to manage load more in SwiftUI without using custom action like loadmore button.
It's better to use ForEach and List for this purpose
struct ContentView : View {
#State var textfieldText: String = "String "
private let chunkSize = 10
#State var range: Range<Int> = 0..<1
var body: some View {
List {
ForEach(range) { number in
Text("\(self.textfieldText) \(number)")
}
Button(action: loadMore) {
Text("Load more")
}
}
}
func loadMore() {
print("Load more...")
self.range = 0..<self.range.upperBound + self.chunkSize
}
}
In this example each time you press load more it increases range of State property. The same you can do for BindableObject.
If you want to do it automatically probably you should read about PullDownButton(I'm not sure if it works for PullUp)
UPD:
As an option you can download new items by using onAppear modifier on the last cell(it is a static button in this example)
var body: some View {
List {
ForEach(range) { number in
Text("\(self.textfieldText) \(number)")
}
Button(action: loadMore) {
Text("")
}
.onAppear {
DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 10)) {
self.loadMore()
}
}
}
}
Keep in mind, that dispatch is necessary, because without it you will have an error saying "Updating table view while table view is updating). Possible you may using another async way to update the data
If you want to keep using List with Data instead of Range, you could implement the next script:
struct ContentView: View {
#State var items: [Landmark]
var body: some View {
List {
ForEach(self.items) { landmark in
CategoryItem(landmark: landmark)
.onAppear {
checkForMore(landmark)
}
}
}
}
func checkForMore(_ item: LandMark) {
guard let item = item else { return }
let thresholdIndex = items.index(items.endIndex, offsetBy: -5)
if items.firstIndex(where: { $0.id == item.id }) == thresholdIndex {
// function to request more data
getMoreLandMarks()
}
}
}
Probably you should work in a ViewModel and separate the logic from the UI.
Credits to Donny Wals: Complete example

Sencha touch 2 custom list item button tap not fired

I am developing a Hybrid Mobile App over sencha touch 2.
Now I was in a need of a custom component to be specific a custom list item consisting of a button along with.
My view has rendered as i wanted to but the button that is added to the list item is not firing the TAP event as expected. Instead on every tap, the ITEMTAP event is fired which is creating a bit of mess.
Can someone suggest me where to look to make this work ?
Below is the code for the custom component that i created:
var listView = {
xtype : "list",
id : "desk-list-search-results",
cls : "desk-list-search-results-cls",
selectedCls : "",
defaultType:"desksearchlistitem",
store : "deskstore",
flex : 2
};
This is the code for the custom component
Ext.define("MyApp.view.DeskSearchListItem",{
extend:"Ext.dataview.component.ListItem",
requires:["Ext.Button"],
alias:"widget.desksearchlistitem",
initialize:function()
{
this.callParent(arguments);
},
config:{
layout:{
type:"hbox",
align:"left"
},
cls:'x-list-item desk-search-list-item',
title:{
cls:"desk-list-item",
flex:0,
styleHtmlContent:true,
style:"align:left;"
},
image:{
cls:"circle_image",
width:"28px",
height:"28px"
},
button:{
cls:'x-button custom-button custom-font bookdesk-button',
flex:0,
text:"Book",
width:"113px",
height:"46px",
hidden:true
},
dataMap:{
getTitle:{
setHtml:'title'
}
}
},
applyButton:function(config)
{
return Ext.factory(config,Ext.Button,this.getButton());
},
updateButton:function(newButton,oldButton)
{
if(newButton)
{
this.add(newButton);
}
if(oldButton)
{
this.remove(oldButton);
}
},
applyTitle:function(config)
{
return Ext.factory(config,Ext.Component,this.getTitle());
},
updateTitle:function(newTitle,oldTitle)
{
if(newTitle)
{
this.add(newTitle);
}
if(oldTitle)
{
this.remove(oldTitle);
}
},
applyImage:function(config)
{
return Ext.factory(config,Ext.Component,this.getImage());
},
updateImage:function(newImage,oldImage)
{
if(newImage)
{
this.add(newImage);
}
if(oldImage)
{
this.remove(oldImage);
}
}
})
Finally got a solution to this,
It can be done into the view using the listeners object.
Here is the code sample for it.
listeners:{
initialize:function()
{
var dataview = this;
dataview.on("itemtap",function(list,index,target,record,event){
event.preventDefault();
});
dataview.on("itemswipe",function(list,index,target,record,event){
if(event.direction === "right")
{
var buttonId = target.element.down(".bookdesk-button").id;
var buttonEl = Ext.ComponentQuery.query("#"+buttonId)[0];
if(Ext.isObject(buttonEl))
{
buttonEl.setZIndex(9999);
buttonEl.show({
showAnimation:{
type:"slideIn",
duration:500
}
});
var listeners = {
tap:function(btn,e,opt)
{
console.log("Button Tapped");
}
};
buttonEl.setListeners(listeners);
}
else
{
console.log("This is not a valid element");
}
}
});
}
}
Thanks Anyways.