swiftui subview reappear after click the back button and update state data - ios

Very strange behavior.
Click the back button on the subpage (Subview) to return to the main page (ContentView). However, the subpage (Subview) automatically opens again. Why?
import SwiftUI
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
var body: some View {
NavigationView{
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
}
.onAppear {
update()
}
.navigationTitle("a")
.toolbar{
NavigationLink(destination: Subview(count: $count), label: {
Text("sub")
})
}
}
}
func update() {
things = []
for i in 0...count {
things.append(String(i))
}
}
}
struct Subview: View {
var count : Binding<Int>
var body: some View {
Text("sub")
.onAppear {
count.wrappedValue += 1
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

NavigationLink should always be inside a NavigationView. If you put it in the toolbar or some other place, you might run into weird issues.
Instead, use the init(destination:isActive:label:) initializer. Then set the presentingNextPage property to true when you want to present the next page.
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
#State var presentingNextPage = false
var body: some View {
NavigationView {
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
/// placeholder navigation link
NavigationLink(destination: Subview(count: $count), isActive: $presentingNextPage) {
EmptyView()
}
}
.onAppear {
self.update()
}
.navigationTitle("a")
.toolbar{
ToolbarItem(placement: .navigationBarTrailing) {
Button("sub") {
presentingNextPage = true /// set to true
}
}
}
}
}
func update() {
things = []
for i in 0...count {
things.append(String(i))
}
}
}
Result:

Put "onAppear{...}" on the NavigationView not the List, like this:
struct ContentView: View {
#State var things: [String] = []
#State var count: Int = 0
var body: some View {
NavigationView{
List {
ForEach(things.indices, id: \.self) { index in
Text(things[index])
}
}
.navigationTitle("a")
.toolbar{
NavigationLink(destination: Subview(count: $count), label: {
Text("sub")
})
}
}
.onAppear { // <---
update()
}
}

Related

SwiftUI NavigationLink in the list doesn't get the right detail page with isActive

I just want to simply navigate to a detail page from a List if press any cell. I have a list like this:
When I click cell c it gets d or others. Rather than this page.
Here is my code:
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var isCellSelected = false
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: Text("\(items[index])"),
isActive: $isCellSelected,
label: {
RowView(text: items[index])
})
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct RowView: View {
var text: String
var body: some View {
VStack {
Text(text)
}
}
}
If I remove isActive: $isCellSelected then it works as expected. I need to use isCellSelected to pop back to root view. Not sure how to fix this issue.
Any help? thanks!
EDIT
Update removed isActive and try set selection = nil
truct DetailView: View {
var text: String
#Binding var isCellSelected: Int?
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = nil
}
}
}
}
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: DetailView(text: "\(items[index])", isCellSelected: $selectedTag),
tag: index,
selection: $selectedTag,
label: {
RowView(text: items[index])
})
}
}
}
}
}
When press Back button, doesn't go back.
It is not recommended to share a single isActive state among multiple NavigationLinks.
Why don't you use selection instead of isActive?
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil //<-
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
NavigationLink(
destination: Text("\(items[index])"),
tag: index, //<-
selection: $selectedTag, //<-
label: {
RowView(text: items[index])
})
}
}
}
}
}
You can set nil to selectedTag to pop back. Seems NavigationLink in List does not work as I expect. Searching for workarounds and update if found.
A dirty workaround:
(Tested with Xcode 12.3/iPhone simulator 14.3. Please do not expect this to work on other versions of iOS including future versions.)
struct DetailView: View {
var text: String
#Binding var isCellSelected: Bool
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = false
}
}
}
}
struct Item {
var text: String
var isActive: Bool = false
}
struct ContentView: View {
#State var items = ["a", "b", "c", "d"].map {Item(text: $0)}
#State var listId: Bool = false //<-
var body: some View {
// Text(items.description) // for debugging
NavigationView {
List {
ForEach(items.indices) { index in
NavigationLink(
destination:
DetailView(text: "\(items[index].text)",
isCellSelected: $items[index].isActive)
.onAppear{ listId.toggle() } //<-
,
isActive: $items[index].isActive,
label: {
RowView(text: items[index].text)
})
}
}
.id(listId) //<-
}
}
}
Another workaround:
struct DetailView: View {
var text: String
#Binding var isCellSelected: Int?
var body: some View {
VStack {
Text(text)
Button("Back") {
isCellSelected = nil
}
}
}
}
struct ContentView: View {
var items = ["a", "b", "c", "d"]
#State var selectedTag: Int? = nil
var body: some View {
NavigationView {
ZStack {
ForEach(items.indices) { index in
NavigationLink(
destination:
DetailView(text: "\(items[index])",
isCellSelected: $selectedTag),
tag: index,
selection: $selectedTag,
label: {
EmptyView()
})
}
List {
ForEach(items.indices) { index in
Button(action: {
selectedTag = index
}) {
HStack {
RowView(text: items[index])
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(Color.secondary)
}
}
}
}
}
}
}
}

Swift - Update List from different View

I have 2 Views in my Swift Project and when I click on the Button on the secondView, I want to update the List in the First View. I don't know how to do it! If I use a static variable in my MainView and then edit this variable from the secondView, it works, but it won't update. And if I don't use static and instead use #State, it would update, but I can't access it from my secondView.
Here is the Code below:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
MainView()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
var arrayList: [CreateListItems] = []
init() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(arrayList) { x in
Text("\(x.name)")
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
func updateList() {
//Code that should remove "FirstName" from the List in MainView
}
var body: some View {
return VStack {
Button(action: {
updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}
struct CreateListItems: Identifiable {
var id: UUID = UUID()
var name: String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can share it using #State and #Binding if you put
struct ContentView: View {
#State var arrayList: [CreateListItems] = []
struct MainView: View {
#Binding var arrayList: [CreateListItems]
struct UpdateOtherViewFromHere: View {
#Binding var arrayList: [CreateListItems]
or you use the MVVM pattern and store the list in an ObservableObject and use #StateObject/#ObservedObject (source) and use #EnvironmentObject(connection) to share it between your Views.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
class ParentViewModel: ObservableObject{
#Published var arrayList: [CreateListItems] = []
init(){
addSamples()
}
func addSamples() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
func updateList() {
let a = CreateListItems(name: "\(arrayList.count + 1) Name!")
arrayList.append(a)
}
}
struct ParentView: View {
#StateObject var vm: ParentViewModel = ParentViewModel()
var body: some View {
TabView {
MainView().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(vm.arrayList) { x in
Text(x.name)
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
Button(action: {
vm.updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}

How to make NavigationLink work if it is not visible, SwiftUI?

When using NavigationLink on the bottom of a view after ForEach it won't work if it is not visible.
I have a list of Buttons. If a button is pressed, it sets a Bool to true. This bool value now shows a NavigationLink which immediately activates because the passed binding is set to true.
However, the link won't work if the array is too long because it will be out of sight once one of the first buttons is pressed.
This is my Code:
import SwiftUI
struct TestLinkView: View {
#State private var linkIsActive = false
var body: some View {
NavigationView {
VStack {
Button(action: {
linkIsActive = true
}) {
Text("Press")
}
NavigationLink(destination: ListView(linkIsActive: $linkIsActive), isActive: $linkIsActive) {
Text("Navigation Link")
}
}
}
}
}
struct ListView: View {
var nameArray = ["Name1","Name2","Name3","Name4","Name5","Name6","Name7","Name8","Name9","Name10","Name11","Name12","Name13","Name14","Name15","Name16","Name17","Name18","Name19","Name20" ]
#State private var showLink: Bool = false
#State private var selectedName: String = ""
#Binding var linkIsActive: Bool
var body: some View {
Form {
ForEach(nameArray, id: \.self) { name in
Button(action: {
selectedName = name
showLink = true
}) {
Text(name)
}
}
if showLink {
NavigationLink(destination: NameView(selectedName: selectedName), isActive: $linkIsActive) {
EmptyView()
}
}
}
.navigationBarTitle("ListView")
}
}
struct NameView: View {
var selectedName: String
var body: some View {
Text(selectedName)
.navigationBarTitle("NameView")
}
}
What would work is to pass the NavigationLink with the if-condition inside the button label. However if I do that, the animation won't work anymore.
You don't need it in Form, which is like a List don't create views far outside of visible area. In your case the solution is to just move link into background of Form (because it does not depend on form internals).
The following tested as worked with Xcode 12 / iOS 14.
Form {
ForEach(nameArray, id: \.self) { name in
Button(action: {
selectedName = name
showLink = true
}) {
Text(name)
}
}
}
.background(Group{
if showLink {
NavigationLink(destination: NameView(selectedName: selectedName), isActive: $linkIsActive) {
EmptyView()
}
}
})

Making a combine passthrough publisher less global

Swift 5, iOS 13
I want to use passthroughSubject publisher; but I my gut tells me its a global variable and as such very poor practice. How can make this global variable less global, while still being usable. Here's some code to show what I talking about.
I know there are a dozen other ways to do this, but I wanted to create some simple code to illustrate the issue.
import SwiftUI
import Combine
let switcher = PassthroughSubject<Void,Never>()
struct SwiftUIViewF: View {
#State var nextPage = false
var body: some View {
VStack {
Text("Switcher")
.onReceive(switcher) { (_) in
self.nextPage.toggle()
}
if nextPage {
Page1ViewF()
} else {
Page2ViewF()
}
}
}
}
struct Page1ViewF: View {
var body: some View {
Text("Page 1")
.onTapGesture {
switcher.send()
}
}
}
struct Page2ViewF: View {
var body: some View {
Text("Page 2")
.onTapGesture {
switcher.send()
}
}
}
struct SwiftUIViewF_Previews: PreviewProvider {
static var previews: some View {
SwiftUIViewF()
}
}
Here is possible solution - to hold it in parent and inject into child views:
struct SwiftUIViewF: View {
let switcher = PassthroughSubject<Void,Never>()
#State var nextPage = false
var body: some View {
VStack {
Text("Switcher")
.onReceive(switcher) { (_) in
self.nextPage.toggle()
}
if nextPage {
Page1ViewF(switcher: switcher)
} else {
Page2ViewF(switcher: switcher)
}
}
}
}
struct Page1ViewF: View {
let switcher: PassthroughSubject<Void,Never>
var body: some View {
Text("Page 1")
.onTapGesture {
self.switcher.send()
}
}
}
struct Page2ViewF: View {
let switcher: PassthroughSubject<Void,Never>
var body: some View {
Text("Page 2")
.onTapGesture {
self.switcher.send()
}
}
}
An example using #EnvironmentObject.
Let SDK take care of observing / passing things for you, rather than setting up yourself.
Especially when your usage is a simple toggle.
import SwiftUI
import Combine
final class EnvState: ObservableObject { #Published var nextPage = false }
struct SwiftUIViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
VStack {
Text("Switcher")
if env.nextPage {
Page1ViewF()
} else {
Page2ViewF()
}
}
}
}
struct Page1ViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
Text("Page 1")
.onTapGesture {
env.nextPage.toggle()
}
}
}
struct Page2ViewF: View {
#EnvironmentObject var env: EnvState
var body: some View {
Text("Page 2")
.onTapGesture {
env.nextPage.toggle()
}
}
}
struct SwiftUIViewF_Previews: PreviewProvider {
static var previews: some View {
SwiftUIViewF().environmentObject(EnvState())
}
}

SwiftUI Picker in a Form doesn't show the selected row

I am trying to have a Picker that shows which option is currently selected.
Try out the following code which correctly selects the right option but the picker does not show which option is selected:
import SwiftUI
struct ContentView: View {
#State var selectedIndex: Int = 0
let strings: [String] = {
var strings: [String] = []
for i in 0..<10 {
strings.append("\(i)")
}
return strings
}()
var body: some View {
NavigationView {
VStack {
Form {
Picker(selection: $selectedIndex,
label: Text("Selected string: \(strings[selectedIndex])")) {
ForEach(0..<strings.count) {
Text(self.strings[$0]).tag($0)
}
}
}
}
.navigationBarTitle("Form Picker",
displayMode: NavigationBarItem.TitleDisplayMode.inline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Anyone know what could be wrong? It's observed using Xcode 11.1 and iOS 13.1
I created the simple picker I call "ListPicker" which should fit the bill. I've written it so it works well in a Form; if you need it outside of a Form you will have to tinker with it. If you see any way to improve the code, please add a comment; this is still a learning experience for all of us.
// MARK: - LIST PICKER (PUBLIC)
struct ListPicker<Content: View>: View {
#Binding var selectedItem: Int
var label: () -> Content
var data: [Any]
var selectedLabel: String {
selectedItem >= 0 ? "\(data[selectedItem])" : ""
}
var body: some View {
NavigationLink(destination: ListPickerContent(selectedItem: self.$selectedItem, data: self.data)) {
ListPickerLabel(label: self.label, value: "\(self.selectedLabel)")
}
}
}
// MARK: - INTERNAL
private struct ListPickerLabel<Content: View>: View {
let label: () -> Content
let value: String
var body: some View {
HStack(alignment: .center) {
self.label()
Spacer()
Text(value)
.padding(.leading, 8)
}
}
}
private struct ListPickerContentItem: View {
let label: String
let index: Int
let isSelected: Bool
var body: some View {
HStack {
Text(label)
Spacer()
if isSelected {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}.background(Color.white) // so the entire row is selectable
}
}
private struct ListPickerContent: View {
#Environment(\.presentationMode) var presentationMode
#Binding var selectedItem: Int
var data: [Any]
var body: some View {
List {
ForEach(0..<data.count) { index in
ListPickerContentItem(label: "\(self.data[index])", index: index, isSelected: index == self.selectedItem).onTapGesture {
self.selectedItem = index
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
Then you can use it like this:
#State var selectedCar: Int = 0
let cars = ["Jaguar", "Audi", "BMW", "Land Rover"]
Form {
ListPicker(
selectedItem: self.$selectedCar,
label: {
Text("Cars")
},
data: self.cars
)
}

Resources