SwiftUI NavigationView pops by itself - ios

it's very strange issue as I cannot reproduce in isolated code, but I hope that someone may think about the reason. I have a view, let's say ContentView that has its ContentViewModel that is ObservedObject, and then there's another View ContentView2. And we have NavigationView in ContentView that wraps navigation link to ContentView2. And it's a bit weird, but when we do some changes that affect ContentViewModel, then NavigationView pops ContentView2 so that we end up in initial ContentView, but we didn't do anything like dismissing ContentView2 or tapping back button. I have a similar code to the one used in my project, but please note that in this code everything works fine:
func qrealm() -> Realm {
return try! Realm(configuration: .init( inMemoryIdentifier: "yw"))
}
class SomeRObj: Object {
#objc dynamic var name: String = ""
convenience init(name: String) {
self.init()
self.name = name
}
static var instance: SomeRObj {
return qrealm().objects(SomeRObj.self).first!
}
}
struct SomeRObjWrapped: Hashable {
var obj: SomeRObj
var prop: Int
}
class ContentViewModel: ObservableObject {
#Published var someRObj: [SomeRObjWrapped] = []
var any: Any?
init() {
let token = qrealm().objects(SomeRObj.self).observe { changes in
switch changes {
case let .initial(data), let .update(data, deletions: _, insertions: _, modifications: _):
let someObjs = data.map { SomeRObjWrapped(obj: $0, prop: Int.random(in: 1..<50)) }
self.someRObj = Array(someObjs)
default: break
}
}
self.any = token
}
}
struct ContentView: View {
#ObservedObject var model: ContentViewModel = ContentViewModel()
var body: some View {
NavigationView {
VStack {
ForEach(model.someRObj, id: \.self) { obj in
Heh(obj: obj.obj, pr: obj.prop)
}
NavigationLink(destination: ContentView2()) {
Text("Link")
}
}
}
}
}
struct Heh: View {
var obj: SomeRObj
var pr: Int
var body: some View {
Text("\(obj.name) \(pr)")
}
}
struct ContentView2: View {
var body: some View {
Button(action: { try! qrealm().write {
let elem = qrealm().objects(SomeRObj.self).randomElement()
elem?.name = "jotaro kujo"
}
}, label: { Text("Toggle") })
}
}

You can replace \.self with \.id:
ForEach(model.someRObj, id: \.id) { obj in
Heh(obj: obj.obj, pr: obj.prop)
}
Then every object will be identified by id and the ForEach loop will only refresh when the id is changed.

Thanks to pawello2222, I found the real reason behind it. I had a NavigationLink inside my List, so that each time there was a change NavigationLink is redrawn and it's state refreshed. I hope that it will be helpfull to someone, and the solution as pawello2222 wrote before is to identify view by id parameter.

Related

.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 update ParentView after updating SubView #ObservedObject SwiftUI

This is a simple example for my case.
I have a #ObservedObject viewModel (Object1), pass a property (Object2) to another view (View2) . Change value in View 2, and when i go back to View 1, i wish the value is updated too. What is the best solution?
In this Example, when i press the blue number, i wish black number is also updated.
Actually I don't know why do the black number is updated after pressing button "Show".
I would really appreciate if you could help me. Thanks.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var object1: Object1 = Object1(ob: Object2(n: 0))
#State var isShow = false
var body: some View {
NavigationView {
VStack {
Text("\(object1.object2.n)")
//NavigationLink(destination: View2(object2: object1.object2)) {
// Text("Go to view 2")
//}
View2(object2: object1.object2)
Button {
isShow = true
} label: {
Text("Show")
}.alert(isPresented: $isShow, content: {
Alert(title: Text("\(object1.object2.n)"))
})
}
}
}
}
struct View2: View {
#ObservedObject var object2: Object2
var body: some View {
Button {
object2.n += 1
} label: {
Text("\(object2.n)")
}
}
}
class Object1: ObservableObject {
#Published var object2: Object2
init(ob: Object2) {
self.object2 = ob
}
}
class Object2: ObservableObject {
#Published var n: Int = 0
init(n: Int) {
self.n = n
}
}
Here is possible solution:
var body: some View {
NavigationView {
VStack {
Text("\(object1.object2.n)")
.onChange(of: object1.object2.n) { _ in
object1.objectWillChange.send()
}
// .. other code
Alternate is to move every object2 dependent part into separated subview observed object2 explicitly.

How to use Dictionary as #Binding var in SwiftUI

I will need to display a collapsed menu in SwiftUI, it is possible to pass one single bool value as binding var to subviews but got stuck when trying to pass that value from a dictionary.
see code below:
struct MenuView: View {
#EnvironmentObject var data: APIData
#State var menuCollapsed:[String: Bool] = [:]
#State var isMenuCollapsed = false;
// I am able to pass self.$isMenuCollapsed but self.$menuCollapsed[menuItem.name], why?
var body: some View {
if data.isMenuSynced {
List() {
ForEach((data.menuList?.content)!, id: \.name) { menuItem in
TopMenuRow(dataSource: menuItem, isCollapsed: self.$isMenuCollapsed)
.onTapGesture {
if menuItem.isExtendable() {
let isCollapsed = self.menuCollapsed[menuItem.name]
self.menuCollapsed.updateValue(!(isCollapsed ?? false), forKey: menuItem.name)
} else {
print("Go to link:\(menuItem.url)")
}
}
}
}
}else {
Text("Loading...")
}
}
}
in ChildMenu Row:
struct TopMenuRow: View {
var dataSource: MenuItemData
#Binding var isCollapsed: Bool
var body: some View {
ChildView(menuItemData)
if self.isCollapsed {
//display List of child data etc
}
}
}
}
If I use only one single bool as the binding var, the code is running ok, however, if I would like to use a dictionary to store each status of the array, it has the error of something else, see image blow:
if I use the line above, it's fine.
Any idea of how can I fix it?
Thanks
How to use dictionary as a storage of mutable values with State property wrapper?
As mentioned by Asperi, ForEach requires that source of data conforms to RandomAccessCollection. This requirements doesn't apply to State property wrapper!
Let see one of the possible approaches in the next snippet (copy - paste - run)
import SwiftUI
struct ContentView: View {
#State var dict = ["alfa":false, "beta":true, "gamma":false]
var body: some View {
List {
ForEach(Array(dict.keys), id: \.self) { (key) in
HStack {
Text(key)
Spacer()
Text(self.dict[key]?.description ?? "false").onTapGesture {
let v = self.dict[key] ?? false
self.dict[key] = !v
}.foregroundColor(self.dict[key] ?? false ? Color.red: Color.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the following result

ForEach in SwiftUI swaps the views (and thus re-using them) between Section instead of updating the data and respecting the view modifiers

struct MasterView: View {
#ObservedObject var store: ModelStore
var body: some View {
List {
Section(header: Text("Black Items")) {
ForEach(store.bag.blackItems, id: \.self) { item in
BlackItemView(model: item)
}
}
Section(header: Text("Blue Items")) {
ForEach(store.bag.blueItems, id: \.self) { item in
BlueItemView(model: item)
}
}
}.navigationBarItems(trailing: Button(action: { self.store.swapItems() }, label: {
Text("Swap items")
}))
}
}
Here is the implementation of swapItems
func swapItems() {
var bagCopy = bag
bagCopy.blackItems = bag.blueItems
bagCopy.blueItems = bag.blackItems
self.bag = bagCopy
}
struct Item: Hashable, Identifiable {
var id: String
}
struct Bag {
var blackItems: [Item] = [Item(id: "item1")]
var blueItems: [Item] = [Item(id: "item2")]
}
class ModelStore: ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
var bag: Bag = Bag() {
didSet {
objectWillChange.send()
}
}
func swapItems() {
var bagCopy = bag
bagCopy.blackItems = bag.blueItems
bagCopy.blueItems = bag.blackItems
self.bag = bagCopy
}
}
Launch the application, the color and item names are what they read.
After tapping "Swap items" button, I expect the items to be swapped between sections, but the color of the labels in the sections should remain the same.
The actual result and the expected output is described in screenshot.
This might be a bug in ForEach, but I would like to get a perspective from the community if there is something wrong in my code snippet.
I did file a bug report to Apple, but in the meanwhile I found a workaround. Not the best one, but it works. The solution is to trick the ForEach that there are new items altogether instead of telling it that the items got swapped. Here is the update to ForEach
List {
Section(header: Text("Black Items")) {
ForEach(store.bag.blackItems, id: \.updatedDate) { item in
BlackItemView(model: item)
}
}
Section(header: Text("Blue Items")) {
ForEach(store.bag.blueItems, id: \.updatedDate) { item in
BlueItemView(model: item)
}
}
}
The trick lies at id parameter. I updated Item to have the second identifier in the form of updatedDate. This will have altogether a new value each time I swap items. This forces ForEach to relinquish the views it is holding and recreate the views afresh. Here is change to Item structure
struct Item: Hashable, Identifiable {
var id: String
var updatedDate: Date = Date()
}
Here is the change to swapItems function in ModelStore
func swapItems() {
var bagCopy = bag
bagCopy.blackItems = bag.blueItems.map { item in
var copy = item
copy.updatedDate = Date()
return copy
}
bagCopy.blueItems = bag.blackItems.map { item in
var copy = item
copy.updatedDate = Date()
return copy
}
self.bag = bagCopy
}
I found a direct answer to your situation. Instead of using value enumaration, you can use INDEX. This will guarantee the result is right.
List {
Section(header: Text("Black Items")) {
ForEach(0..<(store.bag.blackItems.count), id: \.self) { index in
BlackItemView(model: self.store.bag.blackItems[index])
}
}
Section(header: Text("Blue Items")) {
ForEach(0..<(store.bag.blueItems.count), id: \.self) { index in
BlueItemView(model: self.store.bag.blueItems[index])
}
}
}

SwiftUI View - viewDidLoad()?

Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.

Resources