SwiftUI: prevent View from refreshing when presenting a sheet - ios

I have noticed that SwiftUI completely refresh view when adding sheetmodifier.
Let's say I have View that displays random number. I expect that this value would be independent and not connected to the sheet logic (not changing every time I open/close sheet), but every time sheet presented/dismissed Text is changing.
Is it supposed to work so?
Am I wrong that main point of #Sateis to update only connected Views but not all stack?
How can I prevent my View from refreshing itself when presenting a modal?
struct ContentView: View {
#State var active = false
var body: some View {
VStack {
Text("Random text: \(Int.random(in: 0...100))")
Button(action: { self.active.toggle() }) {
Text("Show pop up")
}
}
.sheet(isPresented: $active) {
Text("POP UP")
}
}
}
P.S. ContentView calls onAppear()/onDisappear() and init() only ones.

It needs to make separated condition-independent view to achieve behavior as you wish, like below
struct RandomView: View {
var body: some View {
Text("Random text: \(Int.random(in: 0...100))")
}
}
struct ContentView: View {
#State var active = false
var body: some View {
VStack {
RandomView()
Button(action: { self.active.toggle() }) {
Text("Show pop up")
}
}
.sheet(isPresented: $active) {
Text("POP UP")
}
}
}
In this case RandomView is not rebuilt because is not dependent on active state.

Asperi sad :
View is struct, value type, if any part of it changed then entire
value changed
He is absolutely right! But for that we have state properties. When the view is recreated, the value of state doesn't change.
This should work, as you expected
struct ContentView: View {
#State var active = false
#State var number = Int.random(in: 0 ... 100)
var body: some View {
VStack {
Text("Random text: \(number)")
Button(action: { self.active.toggle() }) {
Text("Show pop up")
}
}
.sheet(isPresented: $active) {
Text("POP UP")
}
}
}
What is the advantage? For simple things, the state / binding is the best solution, without any doubt.
import SwiftUI
struct SheetView: View {
#Binding var randomnumber: Int
var body: some View {
Button(action: {
self.randomnumber = Int.random(in: 0 ... 100)
}) {
Text("Generate new random number")
}
}
}
struct ContentView: View {
#State var active = false
#State var number = Int.random(in: 0 ... 100)
var body: some View {
VStack {
Text("Random text: \(number)")
Button(action: { self.active.toggle() }) {
Text("Show pop up")
}
}
.sheet(isPresented: $active) {
SheetView(randomnumber: self.$number)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Now you can dismiss the sheet with or without generating new random number. No external model is required ...

Related

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()
}
}
})

Creating controls at runtime in SwiftUI

The following code creates new controls every time a button is pressed at runtime, the problem is that the picker selection is set to the same state.
How can I create new controls with different state variables so they can operate separately ?
struct ContentView: View {
#State private var numberOfControlls = 0
#State var selection: String="1"
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
Picker(selection: self.$selection, label:
Text("Picker") {
Text("1").tag(1)
Text("2").tag(2)
}
}
}
}
}
How can I create new controls with different state variables so they can operate separately ?
Separate control into standalone view with own state (or view model if/when needed).
Here is a demo:
struct ContentView: View {
#State private var numberOfControlls = 0
var body: some View {
VStack {
Button(action: {
self.numberOfControlls += 1
}) {
Text("Tap to add")
}
ForEach(0 ..< numberOfControlls, id: \.self) { _ in
ControlView()
}
}
}
}
struct ControlView: View {
#State var selection: String="1"
var body: some View {
Picker(selection: self.$selection, label:
Text("Picker")) {
Text("1").tag(1)
Text("2").tag(2)
}
}
}

Why the first item of the list is displayed all the on the opened sheet

I am passing binding variable into other view:
struct PocketlistView: View {
#ObservedObject var pocket = Pocket()
#State var isSheetIsVisible = false
var body: some View {
NavigationView{
List{
ForEach(Array(pocket.pockets.enumerated()), id: \.element.id) { (index, pocketItem) in
VStack(alignment: .leading){
Text(pocketItem.name).font(.headline)
Text(pocketItem.type).font(.footnote)
}
.onTapGesture {
self.isSheetIsVisible.toggle()
}
.sheet(isPresented: self.$isSheetIsVisible){
PocketDetailsView(pocketItem: self.$pocket.pockets[index])
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Pockets")
}
}
}
the other view is:
struct PocketDetailsView: View {
#Binding var pocketItem: PocketItem
var body: some View {
Text("\(pocketItem.name)")
}
}
Why I see the first item when i open sheet for second or third row?
When I use NavigationLink instead of the .sheet it works perfect
You activate all sheets at once, try the following approach (I cannot test your code, but the idea should be clear)
struct PocketlistView: View {
#ObservedObject var pocket = Pocket()
#State var selectedItem: PocketItem? = nil
var body: some View {
NavigationView{
List{
ForEach(Array(pocket.pockets.enumerated()), id: \.element.id) { (index, pocketItem) in
VStack(alignment: .leading){
Text(pocketItem.name).font(.headline)
Text(pocketItem.type).font(.footnote)
}
.onTapGesture {
self.selectedItem = pocketItem
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Pockets")
.sheet(item: self.$selectedPocket) { item in
PocketDetailsView(pocketItem:
self.$pocket.pockets[self.pocket.pockets.firstIndex(of: item)!])
}
}
}
}

SwiftUI NavigationLink loads destination view immediately, without clicking

With following code:
struct HomeView: View {
var body: some View {
NavigationView {
List(dataTypes) { dataType in
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
}
}
}
}
What's weird, when HomeView appears, NavigationLink immediately loads the AnotherView. As a result, all AnotherView dependencies are loaded as well, even though it's not visible on the screen yet. The user has to click on the row to make it appear.
My AnotherView contains a DataSource, where various things happen. The issue is that whole DataSource is loaded at this point, including some timers etc.
Am I doing something wrong..? How to handle it in such way, that AnotherView gets loaded once the user presses on that HomeViewRow?
The best way I have found to combat this issue is by using a Lazy View.
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
Then the NavigationLink would look like this. You would place the View you want to be displayed inside ()
NavigationLink(destination: NavigationLazyView(DetailView(data: DataModel))) { Text("Item") }
EDIT: See #MwcsMac's answer for a cleaner solution which wraps View creation inside a closure and only initializes it once the view is rendered.
It takes a custom ForEach to do what you are asking for since the function builder does have to evaluate the expression
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
for each visible row to be able to show HomeViewRow(dataType:), in which case AnotherView() must be initialized too.
So to avoid this a custom ForEach is necessary.
import SwiftUI
struct LoadLaterView: View {
var body: some View {
HomeView()
}
}
struct DataType: Identifiable {
let id = UUID()
var i: Int
}
struct ForEachLazyNavigationLink<Data: RandomAccessCollection, Content: View, Destination: View>: View where Data.Element: Identifiable {
var data: Data
var destination: (Data.Element) -> (Destination)
var content: (Data.Element) -> (Content)
#State var selected: Data.Element? = nil
#State var active: Bool = false
var body: some View {
VStack{
NavigationLink(destination: {
VStack{
if self.selected != nil {
self.destination(self.selected!)
} else {
EmptyView()
}
}
}(), isActive: $active){
Text("Hidden navigation link")
.background(Color.orange)
.hidden()
}
List{
ForEach(data) { (element: Data.Element) in
Button(action: {
self.selected = element
self.active = true
}) { self.content(element) }
}
}
}
}
}
struct HomeView: View {
#State var dataTypes: [DataType] = {
return (0...99).map{
return DataType(i: $0)
}
}()
var body: some View {
NavigationView{
ForEachLazyNavigationLink(data: dataTypes, destination: {
return AnotherView(i: $0.i)
}, content: {
return HomeViewRow(dataType: $0)
})
}
}
}
struct HomeViewRow: View {
var dataType: DataType
var body: some View {
Text("Home View \(dataType.i)")
}
}
struct AnotherView: View {
init(i: Int) {
print("Init AnotherView \(i.description)")
self.i = i
}
var i: Int
var body: some View {
print("Loading AnotherView \(i.description)")
return Text("hello \(i.description)").onAppear {
print("onAppear AnotherView \(self.i.description)")
}
}
}
I had the same issue where I might have had a list of 50 items, that then loaded 50 views for the detail view that called an API (which resulted in 50 additional images being downloaded).
The answer for me was to use .onAppear to trigger all logic that needs to be executed when the view appears on screen (like setting off your timers).
struct AnotherView: View {
var body: some View {
VStack{
Text("Hello World!")
}.onAppear {
print("I only printed when the view appeared")
// trigger whatever you need to here instead of on init
}
}
}
For iOS 14 SwiftUI.
Non-elegant solution for lazy navigation destination loading, using view modifier, based on this post.
extension View {
func navigate<Value, Destination: View>(
item: Binding<Value?>,
#ViewBuilder content: #escaping (Value) -> Destination
) -> some View {
return self.modifier(Navigator(item: item, content: content))
}
}
private struct Navigator<Value, Destination: View>: ViewModifier {
let item: Binding<Value?>
let content: (Value) -> Destination
public func body(content: Content) -> some View {
content
.background(
NavigationLink(
destination: { () -> AnyView in
if let value = self.item.wrappedValue {
return AnyView(self.content(value))
} else {
return AnyView(EmptyView())
}
}(),
isActive: Binding<Bool>(
get: { self.item.wrappedValue != nil },
set: { newValue in
if newValue == false {
self.item.wrappedValue = nil
}
}
),
label: EmptyView.init
)
)
}
}
Call it like this:
struct ExampleView: View {
#State
private var date: Date? = nil
var body: some View {
VStack {
Text("Source view")
Button("Send", action: {
self.date = Date()
})
}
.navigate(
item: self.$date,
content: {
VStack {
Text("Destination view")
Text($0.debugDescription)
}
}
)
}
}
I was recently struggling with this issue (for a navigation row component for forms), and this did the trick for me:
#State private var shouldShowDestination = false
NavigationLink(destination: DestinationView(), isActive: $shouldShowDestination) {
Button("More info") {
self.shouldShowDestination = true
}
}
Simply wrap a Button with the NavigationLink, which activation is to be controlled with the button.
Now, if you're to have multiple button+links within the same view, and not an activation State property for each, you should rely on this initializer
/// Creates an instance that presents `destination` when `selection` is set
/// to `tag`.
public init<V>(destination: Destination, tag: V, selection: Binding<V?>, #ViewBuilder label: () -> Label) where V : Hashable
https://developer.apple.com/documentation/swiftui/navigationlink/3364637-init
Along the lines of this example:
struct ContentView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) {
Button("Tap to show second") {
self.selection = "Second"
}
}
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) {
Button("Tap to show third") {
self.selection = "Third"
}
}
}
.navigationBarTitle("Navigation")
}
}
}
More info (and the slightly modified example above) taken from https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui (under "Programmatic navigation").
Alternatively, create a custom view component (with embedded NavigationLink), such as this one
struct FormNavigationRow<Destination: View>: View {
let title: String
let destination: Destination
var body: some View {
NavigationLink(destination: destination, isActive: $shouldShowDestination) {
Button(title) {
self.shouldShowDestination = true
}
}
}
// MARK: Private
#State private var shouldShowDestination = false
}
and use it repeatedly as part of a Form (or List):
Form {
FormNavigationRow(title: "One", destination: Text("1"))
FormNavigationRow(title: "Two", destination: Text("2"))
FormNavigationRow(title: "Three", destination: Text("3"))
}
In the destination view you should listen to the event onAppear and put there all code that needs to be executed only when the new screen appears. Like this:
struct DestinationView: View {
var body: some View {
Text("Hello world!")
.onAppear {
// Do something important here, like fetching data from REST API
// This code will only be executed when the view appears
}
}
}

Dismiss NavigationView when Hidden SwiftUI [duplicate]

I was playing around with SwiftUI and want to be able to come back to the previous view when tapping a button, the same we use popViewController inside a UINavigationController.
Is there a provided way to do it so far ?
I've also tried to use NavigationDestinationLink to do so without success.
struct AView: View {
var body: some View {
NavigationView {
NavigationButton(destination: BView()) {
Text("Go to B")
}
}
}
}
struct BView: View {
var body: some View {
Button(action: {
// Trying to go back to the previous view
// previously: navigationController.popViewController(animated: true)
}) {
Text("Come back to A")
}
}
}
Modify your BView struct as follows. The button will perform just as popViewController did in UIKit.
struct BView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
Button(action: { self.mode.wrappedValue.dismiss() })
{ Text("Come back to A") }
}
}
Use #Environment(\.presentationMode) var presentationMode to go back previous view. Check below code for more understanding.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.gray.opacity(0.2)
NavigationLink(destination: NextView(), label: {Text("Go to Next View").font(.largeTitle)})
}.navigationBarTitle(Text("This is Navigation"), displayMode: .large)
.edgesIgnoringSafeArea(.bottom)
}
}
}
struct NextView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Color.gray.opacity(0.2)
}.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "arrow.left") }))
.navigationBarTitle("", displayMode: .inline)
}
}
struct NameRow: View {
var name: String
var body: some View {
HStack {
Image(systemName: "circle.fill").foregroundColor(Color.green)
Text(name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
With State Variables. Try that.
struct ContentViewRoot: View {
#State var pushed: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination:ContentViewFirst(pushed: self.$pushed), isActive: self.$pushed) { EmptyView() }
.navigationBarTitle("Root")
Button("push"){
self.pushed = true
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentViewFirst: View {
#Binding var pushed: Bool
#State var secondPushed: Bool = false
var body: some View {
VStack{
NavigationLink(destination: ContentViewSecond(pushed: self.$pushed, secondPushed: self.$secondPushed), isActive: self.$secondPushed) { EmptyView() }
.navigationBarTitle("1st")
Button("push"){
self.secondPushed = true;
}
}
}
}
struct ContentViewSecond: View {
#Binding var pushed: Bool
#Binding var secondPushed: Bool
var body: some View {
VStack{
Spacer()
Button("PopToRoot"){
self.pushed = false
} .navigationBarTitle("2st")
Spacer()
Button("Pop"){
self.secondPushed = false
} .navigationBarTitle("1st")
Spacer()
}
}
}
This seems to work for me on watchOS (haven't tried on iOS):
#Environment(\.presentationMode) var presentationMode
And then when you need to pop
self.presentationMode.wrappedValue.dismiss()
There is now a way to programmatically pop in a NavigationView, if you would like. This is in beta 5.
Notice that you don't need the back button. You could programmatically trigger the showSelf property in the DetailView any way you like. And you don't have to display the "Push" text in the master. That could be an EmptyView(), thereby creating an invisible segue.
(The new NavigationLink functionality takes over the deprecated NavigationDestinationLink)
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State var showDetail = false
var body: some View {
VStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Text("Push")
}
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
It seems that a ton of basic navigation functionality is super buggy, which is disappointing and may be worth walking away from for now to save hours of frustration. For me, PresentationButton is the only one that works. TabbedView tabs don't work properly, and NavigationButton doesn't work for me at all. Sounds like YMMV if NavigationButton works for you.
I'm hoping that they fix it at the same time they fix autocomplete, which would give us much better insight as to what is available to us. In the meantime, I'm reluctantly coding around it and keeping notes for when fixes come out. It sucks to have to figure out if we're doing something wrong or if it just doesn't work, but that's beta for you!
Update: the NavigationDestinationLink API in this solution has been deprecated as of iOS 13 Beta 5. It is now recommended to use NavigationLink with an isActive binding.
I figured out a solution for programmatic pushing/popping of views in a NavigationView using NavigationDestinationLink.
Here's a simple example:
import Combine
import SwiftUI
struct DetailView: View {
var onDismiss: () -> Void
var body: some View {
Button(
"Here are details. Tap to go back.",
action: self.onDismiss
)
}
}
struct MainView: View {
var link: NavigationDestinationLink<DetailView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
DetailView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
VStack {
Button("I am root. Tap for more details.", action: {
self.link.presented?.value = true
})
}
.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
struct RootView: View {
var body: some View {
NavigationView {
MainView()
}
}
}
I wrote about this in a blog post here.
You can also do it with .sheet
.navigationBarItems(trailing: Button(action: {
self.presentingEditView.toggle()
}) {
Image(systemName: "square.and.pencil")
}.sheet(isPresented: $presentingEditView) {
EditItemView()
})
In my case I use it from a right navigation bar item, then you have to create the view (EditItemView() in my case) that you are going to display in that modal view.
https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)
EDIT: This answer over here is better than mine, but both work: SwiftUI dismiss modal
What you really want (or should want) is a modal presentation, which several people have mentioned here. If you go that path, you definitely will need to be able to programmatically dismiss the modal, and Erica Sadun has a great example of how to do that here: https://ericasadun.com/2019/06/16/swiftui-modal-presentation/
Given the difference between declarative coding and imperative coding, the solution there may be non-obvious (toggling a bool to false to dismiss the modal, for example), but it makes sense if your model state is the source of truth, rather than the state of the UI itself.
Here's my quick take on Erica's example, using a binding passed into the TestModal so that it can dismiss itself without having to be a member of the ContentView itself (as Erica's is, for simplicity).
struct TestModal: View {
#State var isPresented: Binding<Bool>
var body: some View {
Button(action: { self.isPresented.value = false }, label: { Text("Done") })
}
}
struct ContentView : View {
#State var modalPresented = false
var body: some View {
NavigationView {
Text("Hello World")
.navigationBarTitle(Text("View"))
.navigationBarItems(trailing:
Button(action: { self.modalPresented = true }) { Text("Show Modal") })
}
.presentation(self.modalPresented ? Modal(TestModal(isPresented: $modalPresented)) {
self.modalPresented.toggle()
} : nil)
}
}
Below works for me in XCode11 GM
self.myPresentationMode.wrappedValue.dismiss()
instead of NavigationButton use Navigation DestinationLink
but You should import Combine
struct AView: View {
var link: NavigationDestinationLink<BView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
BView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
NavigationView {
Button(action:{
self.link.presented?.value = true
}) {
Text("Go to B")
}.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
}
struct BView: View {
var onDismiss: () -> Void
var body: some View {
Button(action: self.onDismiss) {
Text("Come back to A")
}
}
}
In the destination pass the view you want to redirect, and inside block pass data you to pass in another view.
NavigationLink(destination: "Pass the particuter View") {
Text("Push")
}

Resources