Swift UI #FocusState triggering NavigationLink Twice - swiftui

What am I doing wrong here? Tapping on the navigation link triggers the navigation, then the focusState value updates, causing body to run and trigger the navigation link again.
How do I prevent the link from being triggered twice causing my destination views init to fire twice?
import SwiftUI
struct ContentView: View {
#State var text: String = "text"
#FocusState var focussed: Bool
#State var isActive: Bool = false
var body: some View {
NavigationView {
List {
TextField("", text: $text)
.focused($focussed)
.onChange(of: focussed) { _ in }
let _ = Self._printChanges()
NavigationLink("Tap Me", destination: MyViewTwo(), isActive: $isActive)
}
}
}
}
struct MyViewTwo: View {
init() {
print("Init Called")
}
var body: some View {
Text("Hello View 2")
}
}

Related

SwiftUI - Navigation title background becomes transparent when VStack is visible

I'm running into an issue with the navigation title header in SwiftUI. It's a combination of a couple of things, as far as I can tell...
The main problem is that I'm trying to change the default background color of a view that contains a list. But when I use the tag .background(), the navigation title background becomes transparent. This only happens when there is a VStack on the view.
I have a simplify example code that shows the problem I'm facing:
ContentView:
import SwiftUI
struct ContentView: View {
#State var showButton: Bool
var body: some View {
VStack {
NavigationStack {
NavigationLink(
destination: SecondView(showButton: showButton),
label: {
Text("Take me to second view")
})
Toggle("VStack Visibile", isOn: $showButton)
.padding()
}
}
}
}
SecondView:
import SwiftUI
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#State var showButton: Bool = true
var body: some View {
VStack {
List(0..<10) { _ in
Text("Hello World")
}
if showButton {
button
}
}
.navigationTitle("This is a title")
.background(Color(.systemCyan))
}
var button: some View {
Text("Something")
}
}
Please see below the resulting problem:
Issues / Suggestions:
ContentView
Have the NavigationStack outside the VStack
SecondView
Don't embed List inside a VStack
List is special and has special characteristics
Don't initialise #State property from outside, pass a binding instead
Code:
ContentView:
struct ContentView: View {
#State var showButton = true
var body: some View {
NavigationStack {
VStack {
NavigationLink(
destination: SecondView(showButton: $showButton),
label: {
Text("Take me to second view")
})
Toggle("VStack Visibile", isOn: $showButton)
.padding()
}
}
}
}
SecondView
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#Binding var showButton: Bool
var body: some View {
List {
ForEach(0..<100) { _ in
Text("Hello World")
}
}
.safeAreaInset(edge: .bottom) {
if showButton {
HStack {
Spacer()
button
Spacer()
}
//I have added transparency, you can make it opaque if you want
.background(.cyan.opacity(0.8))
}
}
}
var button: some View {
Text("Something")
}
}
Try this if you don't want your list go under nav bar.
struct SecondView: View {
#State private var isButtonVisible: Bool = false
#State var showButton: Bool = true
var body: some View {
VStack {
List(0..<10) { _ in
Text("Hello World")
}
.padding(.top, 1)
if showButton {
button
}
}
.background(Color(.systemCyan))
.navigationTitle("This is a title")
}
var button: some View {
Text("Something")
}
}

Reset NagivationView stack in TabView

I have a tabview with two tabs (tabs A and B).
Clicking tab A opens a master View. In that master view there is a navigation link to Page 1. Within Page 1 there is also a link to Page 2.
When the user is on Page 1 or 2, and I tap Tab A, it doesn’t revert to master View. Similarly if the user clicks Tab B and then Tab A again, it returns to Page 1 or 2 (whichever the user was on), rather than master View.
How to I make the navigation stack reset in both cases?
Thanks!
That's because the View won't be rerendered. Here is a possible approach how to achieve your behavior:
You can use ProxyBinding for the TabView to detect changes and then reset the NavigationLink by changing the internal State variable.
struct ContentView: View {
#State var activeView: Int = 0
#State var showNavigation: Bool = false
var body: some View {
TabView(selection: Binding<Int>(
get: {
activeView
}, set: {
activeView = $0
showNavigation = false //<< when pressing Tab Bar Reset Navigation View
}))
{
NavigationView {
NavigationLink("Click", destination: Text("Page A"), isActive: $showNavigation)
}
.tabItem {
Image(systemName: "1.circle")
Text("First")
}
.tag(0)
Text("Second View")
.padding()
.tabItem {
Image(systemName: "2.circle")
Text("Second")
}
.tag(1)
}
}
}
You can create RootView with MainView
import SwiftUI
struct RootView: View {
#ObservedObject var viewModel = RootViewModel()
init(){
viewModel.prepare()
}
var body: some View {
MainView(tab: viewModel.mainTab)
.id(UUID().uuidString)
}
}
Create RootViewModel with listeners to screen updating
import SwiftUI
class RootViewModel: ObservableObject{
#Published var mainTab: SelectedTab = .firstTab
let mainScreenNotification = NSNotification.Name("mainScreenNotification")
private var observerMain: Any?
func prepare(){
observerMain = NotificationCenter.default.addObserver(forName: mainScreenNotification, object: nil, queue: nil, using: { [unowned self] notification in
self.mainTab = (notification.userInfo?["selectedTab"])! as! SelectedTab
})
}
}
enum SelectedTab {
case firstTab, secondTab
}
Run this to inflating new tab screen from tab child:
NotificationCenter.default.post(name:
NSNotification.Name("mainScreenNotification"),
object: nil,
userInfo: ["selectedTab": SelectedTab.firstTab]
)

editMode not working in case of .sheet or .fullScreenCover but with NavigationLink

I'm trying to enable editMode, i.e to come from here:
to here :
with the following code:
struct DetailSheet: View {
#State private var items: [Item] = (0..<5).map { Item(title: "Item #\($0)") }
var body: some View {
NavigationView { // this line to be deleted for using navigationLink
List {
ForEach(items) { item in
Text(item.title)
}
.onMove { (from, to) in
print("just a dummy")
}
} // List
.navigationBarTitle("List")
.navigationBarItems(leading: EditButton())
} // this line to be deleted for using navigationLink
}
}
This works fine, if it is the first view being called from scenedelegate. If I request it from a different "first" view with
.sheet(isPresented: $showDetailSheet, content: {DetailSheet()})
or
.fullScreenCover(isPresented: $showDetailSheet, content: {DetailSheet()})
It does not work anymore. It does work again, if I request the view with a NavigationLink ( if I delete the two lines for NavigationView{ and }.
Am I doing something wrong? If not, can anyone explain me, why editMode doesn't work, if the view was requested by .sheet?
Thanks in advance!
Edit: This is the code of the view (which is called from SceneDelegate) from where I call up DetailView
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State var showDetailSheet : Bool = false
var body: some View {
NavigationView{
List {
NavigationLink(destination: DetailSheet(), label:{ Text("naviDetailSheet")})
Button(action: {showDetailSheet = true}){Text("DetailSheet")}
} // List
.navigationBarItems(trailing: EditButton())
} // Navi
.sheet(isPresented: $showDetailSheet, content: {DetailSheet()})
// .fullScreenCover(isPresented: $showDetailSheet, content: {DetailSheet()})
}
}

SwiftUI: button in ToolbarItem(placement: .principal) not work after change it's label

Xcode 12 beta 6
There is a button in toolbar, its label text is binding to a state var buttonTitle. I want to tap this button to trigger a sheet view, select to change the binding var.
After back to content view, the button's title is updated. But if you tap the button again, it not work.
Code:
struct ContentView: View {
#State var show = false
#State var buttonTitle = "button A"
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .principal) {
Button {
show.toggle()
} label: {
Text(buttonTitle)
}
.sheet(isPresented: $show) {
SelectTitle(buttonTitle: $buttonTitle)
}
}
}
}
}
}
struct SelectTitle: View {
#Environment(\.presentationMode) var presentationMode
#Binding var buttonTitle: String
var body: some View {
Button("Button B") {
buttonTitle = "Button B"
presentationMode.wrappedValue.dismiss()
}
}
}
It is known toolbar-sheet layout issue, see also here. You can file another feedback to Apple.
Here is a workaround for your case - using callback to update toolbar item after sheet closed. Tested with Xcode 12b5.
struct ContentView: View {
#State var show = false
#State var buttonTitle = "button A"
var body: some View {
NavigationView {
Text("Hello World!")
.toolbar {
ToolbarItem(placement: .principal) {
Button {
show.toggle()
} label: {
Text(buttonTitle)
}
.sheet(isPresented: $show) {
SelectTitle(buttonTitle: buttonTitle) {
self.buttonTitle = $0
}
}
}
}
}
}
}
struct SelectTitle: View {
#Environment(\.presentationMode) var presentationMode
#State private var buttonTitle: String
let callback: (String) -> ()
init(buttonTitle: String, callback: #escaping (String) -> ()) {
_buttonTitle = State(initialValue: buttonTitle)
self.callback = callback
}
var body: some View {
Button("Button B") {
buttonTitle = "Button B"
presentationMode.wrappedValue.dismiss()
}
.onDisappear {
callback(buttonTitle)
}
}
}
Move sheet(...) outside of ToolbarItem scope like this:
NavigationView {
..
}.sheet(...)

Is it possible for a NavigationLink to perform an action in addition to navigating to another view?

I'm trying to create a button that not only navigates to another view, but also run a function at the same time. I tried embedding both a NavigationLink and a Button into a Stack, but I'm only able to click on the Button.
ZStack {
NavigationLink(destination: TradeView(trade: trade)) {
TradeButton()
}
Button(action: {
print("Hello world!") //this is the only thing that runs
}) {
TradeButton()
}
}
You can use .simultaneousGesture to do that. The NavigationLink will navigate and at the same time perform an action exactly like you want:
NavigationLink(destination: TradeView(trade: trade)) {
Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
print("Hello world!")
})
You can use NavigationLink(destination:isActive:label:). Use the setter on the binding to know when the link is tapped. I've noticed that the NavigationLink could be tapped outside of the content area, and this approach captures those taps as well.
struct Sidebar: View {
#State var isTapped = false
var body: some View {
NavigationLink(destination: ViewToPresent(),
isActive: Binding<Bool>(get: { isTapped },
set: { isTapped = $0; print("Tapped") }),
label: { Text("Link") })
}
}
struct ViewToPresent: View {
var body: some View {
print("View Presented")
return Text("View Presented")
}
}
The only thing I notice is that setter fires three times, one of which is after it's presented. Here's the output:
Tapped
Tapped
View Presented
Tapped
NavigationLink + isActive + onChange(of:)
// part 1
#State private var isPushed = false
// part 2
NavigationLink(destination: EmptyView(), isActive: $isPushed, label: {
Text("")
})
// part 3
.onChange(of: isPushed) { (newValue) in
if newValue {
// do what you want
}
}
This works for me atm:
#State private var isActive = false
NavigationLink(destination: MyView(), isActive: $isActive) {
Button {
// run your code
// then set
isActive = true
} label: {
Text("My Link")
}
}
Use NavigationLink(_:destination:tag:selection:) initializer and pass your model's property as a selection parameter. Because it is a two-way binding, you can define didset observer for this property, and call your function there.
struct ContentView: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationView {
List(0 ..< 10, id: \.self) { row in
NavigationLink(destination: DetailView(id: row),
tag: row,
selection: self.$navigationModel.linkSelection) {
Text("Link \(row)")
}
}
}
}
}
struct DetailView: View {
var id: Int;
var body: some View {
Text("DetailView\(id)")
}
}
class NavigationModel: ObservableObject {
#Published var linkSelection: Int? = nil {
didSet {
if let linkSelection = linkSelection {
// action
print("selected: \(String(describing: linkSelection))")
}
}
}
}
It this example you need to pass in your model to ContentView as an environment object:
ContentView().environmentObject(NavigationModel())
in the SceneDelegate and SwiftUI Previews.
The model conforms to ObservableObject protocol and the property must have a #Published attribute.
(it works within a List)
I also just used:
NavigationLink(destination: View()....) {
Text("Demo")
}.task { do your stuff here }
iOS 15.3 deployment target.