How to jump from one detail page to another and return to the list page in SwiftUI? - ios

When I use the "next article" button to jump to the article details page with index 3, I want to go directly back to the article list page instead of the article details page with index 2.I tried to search for methods to return to the specified page and destroy the page, but I didn't find them.How to achieve this effect in swiftui?Thanks.I guess the same scenario will happen in other mobile development, right?
The ArticleListView is :
struct ArticleListView: View {
#EnvironmentObject var modelData:ModelData
var body: some View {
NavigationView{
List{
ForEach(modelData.articleList){ article in
NavigationLink(destination:ArticleDetail(index:article.index)){
ArticleItem(index:article.index);
}
}
}
.listStyle(PlainListStyle())
}
}
}
The ArticleDetail is like this:
struct ArticleDetail: View {
#EnvironmentObject var modelData:ModelData
var index:Int
var body: some View {
VStack{
Text(modelData.articleList[index].htmlText)
NavigationLink(destination:ArticleDetail(index:self.index+1)){
Text("next article")
}
}
}
}
The Article/ArticleItemView/ModelData is like this:
struct Article:Identifiable{
var id = UUID()
var index:Int
var htmlText:String
}
struct ArticleItem: View {
#EnvironmentObject var modelData:ModelData
var index:Int
var body: some View {
Text(modelData.articleList[index].htmlText)
}
}
final class ModelData:ObservableObject {
#Published var articleList = [Article(index:0,htmlText: "first test text "),Article(index:1,htmlText: "second test text"),Article(index:2,htmlText: "third test text")]
}

This solution has some potential scalability issues, but it gets the basic job done:
struct Article {
var id = UUID()
}
struct ContentView: View {
var articles = [Article(), Article(), Article(), Article()]
#State private var activeId : UUID?
func activeBinding(id: UUID) -> Binding<Bool> {
.init { () -> Bool in
activeId == id
} set: { (newValue) in
activeId = newValue ? id : nil
}
}
var body: some View {
NavigationView {
VStack(alignment: .leading, spacing: 20) {
ForEach(articles, id: \.id) { article in
NavigationLink(destination: ArticleView(article: article,
articles: articles,
popToTop: { activeId = nil }),
isActive: activeBinding(id: article.id)) {
Text("Link to article: \(article.id)")
}
}
}
}
}
}
struct ArticleView : View {
var article : Article
var articles : [Article]
var popToTop: () -> Void
var body : some View {
VStack(alignment: .leading, spacing: 20) {
Text("Current: \(article.id)")
Button("Pop") {
popToTop()
}
ForEach(articles, id: \.id) { listArticle in
NavigationLink(destination: ArticleView(article: article, articles: articles, popToTop: popToTop)) {
Text("Link to article: \(listArticle.id)")
}
}
}
}
}
On the main page, the top-level article ID is stored in a #State variable. That is tied with a custom binding to an isActive property on the top-level link. Basically, when the article is active, the link is presented and when activeId is nil, the link becomes inactive, and pops to the top.
Because that's the top level view, any views lower in the stack will get popped off if that top-level NavigationLink is inactive.
popToTop is a function that gets passed down to the subsequent article views and gets called if the "Pop" button is pressed.

Related

Infinite loop by using #Binding when passing data between views

High-level description:
There is a nested view problem when a state object is being passed through views. At the end of the deepest view in the hierarchy, the app is frozen and memory consumption is increasing continuously.
Use-case
Partners list → Partner detail → (Locations list) → Location detail
Code-snippets
class PartnerViewModel: ObservableObject {
#Published var partners: [Partner] = Partner.partners
}
This view is loaded into a TabView and a NavigationStack components in the parent class.
struct PartnerListView: View {
#StateObject var viewModel = PartnerViewModel()
var body: some View {
List($viewModel.partners, id: \.self) { $partner in
NavigationLink {
PartnerDetailView(partner: $partner)
} label: {
Text(partner.name)
}
}
}
}
struct PartnerDetailView: View {
#Binding var partner: Partner
var body: some View {
Form {
Section("Locations") {
List($partner.locations, id: \.self) { $location in
NavigationLink {
LocationDetailView(location: $location)
} label: {
Text(location.name)
}
}
}
}
}
}
struct LocationDetailView: View {
#Binding var location: Location
var body: some View {
TextField("Name", text: $location.name)
}
}
The following snippets are workaround and it works but it might be temporary because I don't understand why the first attempt doesn't work and why this one does. I haven't found any resources that could give an example of this scenario.
struct PartnerDetailView: View {
#Binding var partner: Partner
var body: some View {
Form {
Section("Locations") {
List($partner.locations, id: \.self) { $location in
NavigationLink {
LocationDetailView(partner: $partner, locationIndex: partner.locations.firstIndex(of: location) ?? 0)
} label: {
Text(location.name)
}
}
}
}
}
}
struct LocationDetailView: View {
#Binding var partner: Partner
var locationIndex: Int
var body: some View {
TextField("Name", text: $partner.locations[locationIndex].name)
}
}
Is it possible that I am not passing values between views properly?🤔

Updating a binding value pops back to the parent view in the navigation stack

I am passing a Person binding from the first view to the second view to the third view, when I update the binding value in the third view it pops back to the second view, I understand that SwiftUI updates the views that depend on the state value, but is poping the current view is the expected behavior or I am doing something wrong?
struct Person: Identifiable {
let id = UUID()
var name: String
var numbers = [1, 2]
}
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct SecondView: View {
#Binding var person: Person
var body: some View {
Form {
NavigationLink(destination: ThirdView(person: $person)) {
Text("Update Info")
}
}
}
}
struct ThirdView: View {
#Binding var person: Person
var body: some View {
Form {
Button(action: {
person.numbers.append(3)
}) {
Text("Append a new number")
}
}
}
}
When navigating twice you need to either use isDetailLink(false) or StackNavigationViewStyle, e.g.
struct FirstView: View {
#State private var people = [Person(name: "Current Name")]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: SecondView(person: $person)) {
Text(person.name)
}
.isDetailLink(false) // option 1
}
}
.navigationViewStyle(.stack) // option 2
}
}
SwiftUI works by updating the rendered views to match what you have in your state.
In this case, you first have a list that contains an element called Current Name. Using a NavigationLink you select this item.
You update the name and now that previous element no longer exists, it's been replaced by a new element called New Name.
Since Current Name no longer exists, it also cannot be selected any longer, and the view pops back to the list.
To be able to edit the name without popping back, you'll need to make sure that the item on the list is the same, even if the name has changed. You can do this by using an Identifiable struct instead of a String.
struct Person: Identifiable {
let id = UUID().uuidString
var name = "Current Name"
}
struct ParentView: View {
#State private var people = [Person()]
var body: some View {
NavigationView {
List($people) { $person in
NavigationLink(destination: ChildView(person: $person)) {
Text(person.name)
}
}
}
}
}
struct ChildView: View {
#Binding var person: Person
var body: some View {
Button(action: {
person.name = "New Name"
}) {
Text("Update Name")
}
}
}

Is there a way to create objects in swiftUI view based on a value gathered from a previous view?

I have recently started my journey into iOS development learning swift and swift UI. I keep running into issues when it comes to app architecture. The problem i am trying to solve is this: Let's say I have an app where the user first selects a number and then presses next. The user selected number is supposed to represent the number of text fields that appear on the next view. For example, if the user selects 3 then 3 text fields will appear on the next view but if the user selects 5 then 5 texts fields will appear. Is the solution to just have a view for each case? Or is there some way to dynamically add objects to a view based on the user input. Can anyone explain how they would handle a case like this?
Views can get passed parameters (including in NavigationLink) that can determine what they look like. Here's a simple example with what you described:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(numberOfFields: numberOfFields)) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct DetailView : View {
var numberOfFields : Int
var body: some View {
VStack {
ForEach(0..<numberOfFields) { index in
TextField("", text: .constant("Field \(index + 1)"))
}
}
}
}
Notice how numberOfFields is stored as #State in the parent view and then passed to the child view dynamically.
In general, it would probably be a good idea to visit some SwiftUI tutorials as this type of thing will be covered by most of them. Apple's official tutorials are here: https://developer.apple.com/tutorials/swiftui
Another very popular resource is Hacking With Swift: https://www.hackingwithswift.com/100/swiftui
Update, based on comments:
struct ContentView : View {
#State var numberOfFields = 3
var body: some View {
NavigationView {
VStack {
Stepper(value: $numberOfFields, in: 1...5) {
Text("Number of fields: \(numberOfFields)")
}
NavigationLink(destination: DetailView(textInputs: Array(repeating: "test", count: numberOfFields))) {
Text("Navigate")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct Model : Identifiable {
var id = UUID()
var text : String
}
class ViewModel : ObservableObject {
#Published var strings : [Model] = []
}
struct DetailView : View {
var textInputs : [String]
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(Array(viewModel.strings.enumerated()), id: \.1.id) { (index,text) in
TextField("", text: $viewModel.strings[index].text)
}
}.onAppear {
viewModel.strings = textInputs.map { Model(text: $0) }
}
}
}

.fullScreenCover always opening same Detail Page

so I'm having a bit of an issue here I'm hoping is easy to fix, just can't figure it out at the moment. I'm running a loop through some CoreData info (posts) and returning a grid of images, I want to be able to click these images and open up a fullScreenCover of the DetailView with the correct info in it. With the current code, the DetailView always shows the data from the first post. If I change it from a Button to a NavigationLink NavigationLink(destination: DetailView(post: post)), as commented out in the code, it works perfectly, but doesn't give me the fullScreenCover behaviour I would like. What am I doing wrong here? Thanks in advance!
#FetchRequest(entity: Post.entity(), sortDescriptors: []) var posts: FetchedResults<Post>
enum ActiveSheet: Identifiable {
case detail, addNew
var id: Int {
hashValue
}
}
#State var activeSheet: ActiveSheet?
var body: some View {
ForEach(posts.reversed(), id: \.self) { post in
VStack {
Button(action: { activeSheet = .detail }){
//NavigationLink(destination: DetailView(post: post)){
ZStack {
Image(uiImage: UIImage(data: post.mainImage ?? self.image)!)
VStack {
Text("\(post.title)")
Text("\(post.desc)")
}
}
}
.fullScreenCover(item: $activeSheet) { item in
switch item {
case .detail:
DetailView(post: post)
case .addNew:
AddNewView()
}
}
}
}
}
I've made the array of posts static for now instead of coming from Core Data and mocked the objects/structs so that I could test easily, but the principal should stay the same:
struct ContentView : View {
//#FetchRequest(entity: Post.entity(), sortDescriptors: []) var posts: FetchedResults<Post>
var posts : [Post] = [Post(title: "1", desc: "desc1"),
Post(title: "2", desc: "desc2"),
Post(title: "3", desc: "desc3")]
enum ActiveSheet: Identifiable {
case detail(post: Post)
case addNew
var id: UUID {
switch self {
case .detail(let post):
return post.id
default:
return UUID()
}
}
}
#State var activeSheet: ActiveSheet?
var body: some View {
ForEach(posts.reversed(), id: \.self) { post in
VStack {
Button(action: { activeSheet = .detail(post: post) }){
ZStack {
//Image(uiImage: UIImage(data: post.mainImage ?? self.image)!)
VStack {
Text("\(post.title)")
Text("\(post.desc)")
}
}
}
}
.fullScreenCover(item: $activeSheet) { item in
switch item {
case .detail(let post):
DetailView(post: post)
case .addNew:
AddNewView()
}
}
}
}
}
struct DetailView : View {
var post: Post
var body : some View {
Text("Detail \(post.id)")
}
}
struct AddNewView : View {
var body : some View {
Text("add")
}
}
struct Post : Hashable {
var id = UUID()
var title : String
var desc : String
}
The basic idea is that instead of creating the fullScreenCover on first render, you should create it in based on the activeSheet so that it gets created dynamically. You were on the right track using item: and activeSheet already -- the problem was it wasn't tied to the actual post, since you were just using the button to set activeSheet = .detail.
I've added an associated property to case detail that allows you to actually tie a post to it. Then, in fullScreenCover you can see that I use that associated value when creating the DetailView.
You may have to make slight adjustments to fit your Post model, but the concept will remain the same.

How to notify view that the variable state has been updated from a extracted subview in SwiftUI

I have a view that contain users UsersContentView in this view there is a button which is extracted as a subview: RequestSearchButton(), and under the button there is a Text view which display the result if the user did request to search or no, and it is also extracted as a subview ResultSearchQuery().
struct UsersContentView: View {
var body: some View {
ZStack {
VStack {
RequestSearchButton()
ResultSearchQuery(didUserRequestSearchOrNo: .constant("YES"))
}
}
}
}
struct RequestSearchButton: View {
var body: some View {
Button(action: {
}) {
Text("User requested search")
}
}
}
struct ResultSearchQuery: View {
#Binding var didUserRequestSearchOrNo: String
var body: some View {
Text("Did user request search: \(didUserRequestSearchOrNo)")
}
}
How can I update the #Binding var didUserRequestSearchOrNo: String inside the ResultSearchQuery() When the button RequestSearchButton() is clicked. Its so confusing!
You need to track the State of a variable (which is indicating if a search is active or not) in your parent view, or your ViewModel if you want to extract the Variables. Then you can refer to this variable in enclosed child views like the Search Button or Search Query Results.
In this case a would prefer a Boolean value for the tracking because it's easy to handle and clear in meaning.
struct UsersContentView: View {
#State var requestedSearch = false
var body: some View {
ZStack {
VStack {
RequestSearchButton(requestedSearch: $requestedSearch)
ResultSearchQuery(requestedSearch: $requestedSearch)
}
}
}
}
struct RequestSearchButton: View {
#Binding var requestedSearch: Bool
var body: some View {
Button(action: {
requestedSearch.toggle()
}) {
Text("User requested search")
}
}
}
struct ResultSearchQuery: View {
#Binding var requestedSearch: Bool
var body: some View {
Text("Did user request search: \(requestedSearch.description)")
}
}
Actually I couldn't understand why you used two struct which are connected to eachother, you can do it in one struct and Control with a state var
struct ContentView: View {
var body: some View {
VStack {
RequestSearchButton()
}
}
}
struct RequestSearchButton: View {
#State private var clicked : Bool = false
var body: some View {
Button(action: {
clicked = true
}) {
Text("User requested search")
}
Text("Did user request search: \(clicked == true ? "YES" : "NO")")
}
}
if this is not what you are looking for, could you make a detailed explain.

Resources