I'm trying to add "Transition A" to the parent view and "Transition B" to it's child view in SwiftUI but I'm unable to do so, because the child view in SwiftUI always follows the parent's transition.
UPDATE: sample code
Can I do something like this ?
struct ParentView: View {
var body: some View {
ScrollView {
ForEach(items) { item in // items is a struct with properties name & city
ChildView(item: item).transition(.slide)
}
}
}
}
struct ChildView: View {
var item: JSON
var body: some View {
VStack {
Text("Name: \(item.name)").transition(scale)
Text("City: \(item.city)").transition(scale)
}
}
}
How do I achieve this ? Any help would be great cuz I have been struggling with this forever.
Related
When I update a binding property from an array in a pushed view 2+ layers down, the navigation pops back instantly after a change to the property.
Xcode 13.3 beta, iOS 15.
I created a simple demo and code is below.
Shopping Lists
List Edit
List section Edit
Updating the list title (one view deep) is fine, navigation stack stays same, and changes are published if I return. But when adjusting a section title (two deep) the navigation pops back as soon as I make a single change to the property.
I have a feeling I'm missing basic fundamentals here, and I have a feeling it must be related to the lists id? but I'm struggling to figure it out or work around it.
GIF
Code:
Models:
struct ShoppingList {
let id: String = UUID().uuidString
var title: String
var sections: [ShoppingListSection]
}
struct ShoppingListSection {
let id: String = UUID().uuidString
var title: String
}
View Model:
final class ShoppingListsViewModel: ObservableObject {
#Published var shoppingLists: [ShoppingList] = [
.init(
title: "Shopping List 01",
sections: [
.init(title: "Fresh food")
]
)
]
}
Content View:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}
}
}
ShoppingListsView
struct ShoppingListsView: View {
#StateObject private var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
ShoppingListEditView
struct ShoppingListEditView: View {
#Binding var shoppingList: ShoppingList
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $shoppingList.title)
}
Section(header: Text("Sections")) {
List($shoppingList.sections, id: \.id) { $section in
NavigationLink(destination: ShoppingListSectionEditView(section: $section)) {
Text(section.title)
}
}
}
}
.navigationBarTitle("Edit list")
}
}
ShoppingListSectionEditView
struct ShoppingListSectionEditView: View {
#Binding var section: ShoppingListSection
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("title", text: $section.title)
}
}
.navigationBarTitle("Edit section")
}
}
try this, works for me:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}.navigationViewStyle(.stack) // <--- here
}
}
Try to make you object confirm to Identifiable and return value which unique and stable, for your case is ShoppingList.
Detail view seems will pop when object id changed.
The reason your stack is popping back to the root ShoppingListsView is that the change in the list is published and the root ShoppingListsView is registered to listen for updates to the #StateObject.
Therefore, any change to the list is listened to by ShoppingListsView, causing that view to be re-rendered and for all new views on the stack to be popped in order to render the root ShoppingListsView, which is listening for updates on the #StateObject.
The solution to this is to change the #StateObject to #EnvironmentObject
Please refactor your code to change ShoppingListsViewModel to use an #EnvironmentObject wrapper instead of a #StateObject wrapper
You may pass the environment object in to all your child views and also add a boolean #Published flag to track any updates to the data.
Then your ShoppingListView would look as below
struct ShoppingListsView: View {
#EnvironmentObject var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
Don't forget to pass the viewModel in to all your child views.
That should fix your problem.
I'm working through the Apple SwiftUI tutorials, and I'm encountering an issue where child views automatically return to the parent view whenever they change a variable in an #EnvironmentObject.
The parent LandmarkList view functionality allows navigation to any child view in a list of Landmarks, with the ability to filter for only those child views that are "Favorites" of the user.
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
#State private var showFavoritesOnly = true
var filteredLandmarks: [Landmark] {
modelData.landmarks.filter { landmark in
(!showFavoritesOnly || landmark.isFavorite)
}
}
var body: some View {
NavigationView {
List{
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(filteredLandmarks) { landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
.navigationBarTitle("Landmarks")
}
}
}
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("/landmarkData.json")
}
When within the child view (and only when the "Favorites only" is toggled to ON in the parent LandmarkList view), if the user changes the "Favorite" status via the FavoriteButton, the view immediately returns to the parent view. More confusingly, this happens for every child view in the parent LandmarkList EXCEPT whichever Landmark is in the last row (which allows for changing the "Favorite" status on and off without displaying this behavior).
struct LandmarkDetail: View {
#EnvironmentObject var modelData: ModelData
var landmark: Landmark
var landmarkIndex: Int {
modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(landmark.name)
.font(.title)
.foregroundColor(.primary)
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
}
.navigationBarTitle(landmark.name)
}
}
}
I have two views written in swiftUI , say for example ViewA and ViewB.
onAppear() of ViewA has an apiCall which calls when initially the view is loaded.
I navigate to ViewB from ViewA using navigation link and on clicking back button in ViewB the onAppear() of ViewA is called.
• Is there any way to stop calling onAppear() while navigated back from a view
• I am looking swiftUI for something like 'ViewDidLoad' in UIKit
given a sample of my code
struct ContentView: View {
var body: some View {
NavigationView{
List(viewModel.list){ item in
NavigationLink(
destination: Text("Destination"),
label: {
Text(item.name)
})
}
.onAppear{
viewModel.getListApiCall()
}
}
}
}
Overview
SwiftUI is quite different from the way UIKit works.
It would be best to watch the tutorials (links below) to understand how SwiftUI and Combine works.
SwiftUI is a declarative framework so the way we approach is quite different. It would be best not to look for a direct comparison to UIKit for equivalent functions.
Model:
Let the model do all the work of fetching and maintaining the data
Ensure that your model conforms to ObservableObject
When ever any #Published property changes, it would imply that the model has changed
View:
Just display the contents of the model
By using #ObservedObject / #EnvironmentObject SwiftUI would observe the model and ensure that the view states in sync with any changes made to the model
Notice that though the model fetches the data after 2 seconds, the view reacts to it and displays the updated data.
Model Code:
class Model: ObservableObject {
#Published var list = [Item]()
init() {
fetchItems()
}
private func fetchItems() {
//To simulate some Async API call
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.list = (1...10).map { Item(name: "name \($0)") }
}
}
}
struct Item: Identifiable {
var name: String
var id : String {
name
}
}
View Code:
import SwiftUI
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView{
List(model.list){ item in
NavigationLink(destination: Text("Destination")) {
Text(item.name)
}
}
}
}
}
Reference:
SwiftUI
https://developer.apple.com/videos/play/wwdc2020/10119
https://developer.apple.com/videos/play/wwdc2020/10037
https://developer.apple.com/videos/play/wwdc2020/10040
Combine
https://developer.apple.com/videos/play/wwdc2019/722
https://developer.apple.com/videos/play/wwdc2019/721
https://developer.apple.com/videos/play/wwdc2019/226
You could add a variable to check if the getListApiCall() has been invoked.
struct ContentView: View {
#State var initHasRun = false
var body: some View {
NavigationView{
List(viewModel.list){ item in
NavigationLink(
destination: Text("Destination"),
label: {
Text(item.name)
})
}
.onAppear{
if !initHasRun {
viewModel.getListApiCall()
initHasRun=true
}
}
}
}
}
In the code below, if I use the links to go back and forth between views A and B, I will end up with nested views as shown in the image. The only way I've found to avoid nesting is to never link to a view where a NavigationView is declared - as it is in ViewA below. My question...is there any way to go back to ViewA without the views nesting?
struct ViewA: View {
var body: some View {
NavigationView{
NavigationLink(destination: ViewB()) {
Text("ViewB")
}
}
.navigationBarTitle("ViewA")
}
}
struct ViewB: View {
var body: some View {
NavigationLink(destination: ViewA()) {
Text("ViewA")
}
.navigationBarTitle("ViewB")
}
}
You should not create NavigationLink(destination: ViewA()) because it is not back it creates a new ViewA. Once you navigate to ViewB, the back button will be create for you automatically.
struct ViewA: View {
var body: some View {
NavigationView{
NavigationLink(destination: ViewB()) {
Text("ViewB")
}
}
.navigationBarTitle("ViewA")
}
}
struct ViewB: View {
var body: some View {
Text("ViewB Pure Content")
.navigationBarTitle("ViewB")
}
}
You are nesting views because every time you click ViewA/ViewB it creates a new view object. You can add
#Environment(\.presentationMode) var presentationMode
and call
presentationMode.wrappedValue.dismiss()
when the view button gets pressed you dismiss it.
I'm working on a SwiftUI practice app and I ran into an issue with the NavigationView/NavigationLink. I am currently using the Metropolitan Museum of Art API and I wanted to make a list of departments that segues to another list of objects in that department, then that list segues to the object's information. Currently the NavigationView/NavigationLink setup I have is creating multiple NavigationViews and is resulting in multiple back buttons/navigation bars. Is there a way to have the new NavigationView replace the old one or have them work in line with one another? The only way I know how to create a segue in SwiftUI is through a NavigationView/NavigationLink but creating it twice seems to be the wrong way to go about things. I have a screen shot of the current state of my app.
App Image
This is my code at the moment.
struct ContentView: View {
#ObservedObject var model = DepartmentListViewModel()
var body: some View {
NavigationView {
List {
ForEach(model.departments, id: \.self) { department in
NavigationLink(destination: DetailView(viewModel: DetailListViewModel(selectedDepartment: department))) {
Text(department.displayName)
}
}
}
.navigationBarTitle("Departments")
}
}
}
struct DetailView: View {
#ObservedObject var viewModel: DetailListViewModel
init(viewModel: DetailListViewModel) {
self.viewModel = viewModel
}
var body: some View {
NavigationView {
List {
ForEach(viewModel.objects, id: \.self) { object in
NavigationLink(destination: ObjectView(viewModel: ObjectListViewModel(selectedObject: object))) {
Text(String(object))
}
}
}
.navigationBarTitle("ObjectIDs")
}
}
}
You don't need NavigationView in your DetailView anymore, the first one handle it