Show ProgressView when Exporting File in SwiftUI - ios

I am trying to show a ProgressView when trying to export a file. The file is a large file and I am using fileExporter modifier. Having a ProgressView within a ZStack and setting the zIndex to 1 does not work. I also tried to put the long operation (getting the document) into a DispatchQueue but the challenge there is that it is not a View and does not compile. Below is the code that I currently have.
Here is what I am expecting:
After I press the "Export File" button, I should see a spinning circle while the document is being created (in this case the 2 second sleep command that simulates the long operation).
After that the "Move" sheet from fileExporter should be presented
Any help is very much appreciated.
Here is a sample application:
struct ContentView: View {
#State private var isExporting: Bool = false
var body: some View {
VStack {
Button(action: {
isExporting = true
}, label: {
Text("Export File")
})
.padding()
if isExporting {
ProgressView() {
ZStack{}
.fileExporter(isPresented: $isExporting, document: FileExport(contents: "This is a simple Text"), contentType: .plainText) { result in
if case .success = result {
print(try! result.get())
print("File Saved")
} else {
print("File Not Saved")
}
}
}
}
}
}
}
struct FileExport: FileDocument {
static var readableContentTypes: [UTType] {[.plainText]}
var contents: String
init(contents: String) {
self.contents = contents
sleep(2) // simulate a long operation
}
init(configuration: ReadConfiguration) throws {
contents = ""
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
return FileWrapper(regularFileWithContents: contents.data(using: .utf8)!)
}
}

You have a few problems in your code. First of all, your ProgressView() is not actually contained in a ZStack. The only ZStack you are showing does not contain anything, as the contents would have to be inside the curly braces. In a situation like this, you could generally just wrap your entire view in a ZStack.
Next, your own views don't generally take trailing closures like that. You didn't include your ProgressView code, but the code you have shown would be nonsensical to have as a trailing closure, so I don't think that was your intention. So, I removed the open curly brace the appropriate close curly brace.
When all of this is said and done, the view will show, but it will still be below the file export window. I have never found any way of placing a view above an OS sheet like this. I have seen some "solutions" if it is your sheet, but they were not really recommended.
Also, I am not sure how you are getting data back to show a progress view, other than just as a continuous spinner. I could not find anywhere in the docs where Apple gives you access to that data.
Lastly, because you are keying your progress view to the isPresented var isExporting means your ProgressView() will dismiss when the FileExport sheet does.
Also, while it seems to work with the .fileExporter inside the conditional if block, it feels wrong to me, and I would move it to another part of the view. It just seems like a bad practice.
FWIW, here is the "working" code, but I don't see how you can actually accomplish your goals:
struct FileExporter: View {
#State private var isExporting: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
isExporting = true
}, label: {
Text("Export File")
})
.padding()
if isExporting {
ProgressView() // { No brace would go here
.fileExporter(isPresented: $isExporting, document: FileExport(contents: "This is a simple Text"), contentType: .plainText) { result in
if case .success = result {
print(try! result.get())
print("File Saved")
} else {
print("File Not Saved")
}
}
}
}
}
}
}
If you want to implement a solution that shows a progress view (bar or spinner), I think you would have to roll your own.

Here is a possible solution. There may be a better way to do this! I am using a ZStack{}to attach the fileExporter. Is there a different way or other solution?
struct ContentView: View {
#State private var isExporting: Bool = false
#State private var isLoading: Bool = false
#State private var document: FileExport? = nil
var body: some View {
VStack {
Button(action: {
isExporting = true
isLoading = true
DispatchQueue.global(qos: .background).async {
document = FileExport(contents: "This is a simple text")
DispatchQueue.main.async {
isLoading = false
}
}
}, label: {
Text("Export File")
})
.padding()
if isLoading {
ProgressView()
.zIndex(1.0)
}
if isExporting && !isLoading {
ZStack {}
.fileExporter(isPresented: $isExporting, document: document, contentType: .plainText) { result in
if case .success = result {
print(try! result.get())
print("File Saved")
} else {
print("File Not Saved")
}
}
}
}
}
}

Related

The #ObservedResults loads old data in View on deletion, creation or update

Description
I've got simple Combat model which stores name and list of actors. When I delete the Combat from List using onDelete it looks like it's working. It removes the Combat from Realm (checked with RealmStudio) and updates the view. However, if view gets redrawn (for instance, when switching Apps), the "old" data is loaded again (the very first loaded on app initialization), so all deleted rows are back again. Of course, removing them again crashes the app, because they are not present in #ObservedResults combats anymore. Restarting the app fixes the issue, because new data is loaded to #ObservedResults combats and to List, but then again, when I removed something it will be back on review draw...
What I discovered is that removing .sheet() fixes the issue! (EDIT: clarification; it doesn't matter what's inside of the sheet, it may be even empty) The view is updated correctly on redraw! The Sheet is used to display form to add new Combat (nether to say that adding new combats or editing them does not update the view as well, but let's focus on deletion). I have no idea what adding sheet() changes in behaviour of the List and "listening" to #ObservedResults combats.
As a test I used simple array of Combat classes and everything worked. So it points me to issue with #ObservedResults.
I was using the Alert before and all changes to #ObservedResults combats were seen at glance. Now I wanted to replace Alert with Sheet and… That happened.
Also, I have subview where I have almost identical code for actor and there everything works, however I use #ObservedRealmObject var combat: Combat there, and I pass the combat #ObservedResults combats, like so:
NavigationLink(destination: CombatView(combat: combat)) { Text(combat.name) }
I removed unecessary code from below examples to keep it at minimum.
Model
The Combat model:
class Combat: Object, ObjectKeyIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var name: String = ""
var actors = List<Actor>()
}
Actual View Code (broken using Sheet)
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
struct CombatsListView: View {
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
var body: some View {
List {
ForEach(combats) { combat in
Text(combat.name)
}.onDelete(perform: $combats.remove)
}
.sheet(isPresented: $showAddCombat) {
AddCombatView( showAddCombat: $showAddCombat)
}
}
}
Old View Code (works using Alert)
struct CombatsListView: View {
#ObservedResults(
Combat.self,
sortDescriptor: SortDescriptor( keyPath: "name", ascending: true)
) var combats
#State private var showAddCombat = false
#State private var addCombatNewName = ""
var body: some View {
List(combats) { combat in
Text(combat.name)
.onDelete(perform: $combats.remove)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAlert = true
}) {
Image(systemName: "plus" )
.font(.title)
Text("New Combat")
}.alert("New Combat", isPresented: $showAlert) {
TextField("write name", text: $addCombatNewName)
Button("Close", role: .cancel) {
addCombatNewName = ""
}
Button("Add") {
addNewCombat(name: addCombatNewName)
addCombatNewName = ""
}
}
}
}
}
private func addNewCombat(name: String) {
let newCombat = Combat()
newCombat.name = name
do {
try self.realm.write {
realm.add(newCombat)
}
} catch {
fatalError("Error: \(error)")
}
}
}
EDITED
I just found some new behaviour. I made a new simple view which lists elements of Collection list and you can delete or add new Collection. It works just fine, but if I include this CollectionsView under the TabView, then the effect is exactly the same as in the example above. The view stops working properly: deleted items are added back on view redraw and adding new objects doesn't refresh the View.
This makes me think more of a bug in #ObservedResults().
Below is the source code.
class Collection: Object, ObjectKeyIdentifiable {
#objc dynamic var id: String = UUID().uuidString
#objc dynamic var name: String = ""
var actors = List<Actor>()
}
#main
struct CombatTrackerApp: App {
var body: some Scene {
WindowGroup {
Tabber() // will not work
// CollectionsView() // will work
}
}
}
struct CollectionsView: View {
#ObservedResults( Collection.self ) var collections
#State private var showNewCollectionForm = false
var body: some View {
NavigationStack {
List {
ForEach(collections) { collection in
Text(collection.name)
}.onDelete(perform: $collections.remove)
}
.listStyle(.inset)
.padding()
.navigationTitle("Collections")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button() {
self.showNewCollectionForm.toggle()
} label: {
Image(systemName: "plus")
Text("Add New Collection")
}
}
}
.sheet(isPresented: $showNewCollectionForm) {
NewCollectionView( showNewCollectionForm: $showNewCollectionForm )
}
}
}
}
struct NewCollectionView: View {
let realm = try! Realm()
#Binding var showNewCollectionForm: Bool
#State private var newCollectioName: String = ""
var body: some View {
NavigationStack {
VStack {
Text("Create new Collection").font(.title).padding()
Form {
TextField("Name", text: $newCollectioName)
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Close", role: .cancel) {
showNewCollectionForm.toggle()
}
}
ToolbarItem {
Button("Create") {
addCollection()
} .disabled(newCollectioName.isEmpty)
}
}
}
}
private func addCollection() {
let newCollection = Collection()
newCollection.name = newCollectioName
do {
try realm.write {
realm.add(newCollection)
}
} catch {
print("Cannot add new Collection", error)
}
showNewCollectionForm.toggle()
}
}
struct Tabber: View {
var body: some View {
TabView() {
NavigationStack {
CombatsListView()
}
.tabItem {
Text("Combats")
}
NavigationStack {
CollectionsView()
}
.tabItem {
Text("Collections")
}
SettingsView()
.tabItem {
Text("Settings")
}
}
}
}
I found out the solution (but I still don't understand why it's working).
The solution was to move NavigationStack from my TabView to the subviews. So instead of:
struct Tabber: View {
var body: some View {
TabView() {
NavigationStack {
CombatsListView()
}
.tabItem {
Text("Combats")
}
//...
I should do:
struct Tabber: View {
var body: some View {
TabView() {
CombatsListView()
.tabItem {
Text("Combats")
}
//...
struct CombatsListView: View {
var body: some View {
NavigationStack {
Confusing part was that all online tutorials and Apple Documentation suggests to wrap subviews with NavigationStack in TabView directly instead of adding NavigationStack in subviews. Maybe it's a bug, maybe it's a feature.

Use of .fileExporter

i have added a fileExporter to my code so that when the user click the export button, it toggle the value of $showingExporter and expect to show the fileExporter. It works fine, except I found that it will call the method "CreateCSV" in my case no matter whether my button is clicked. The function CreateCSV will be called whenever my View dismiss. Any idea?
var body: someView {
NavigationView {
VStack {
List {
Button(action: {self.showingExporter.toggle()}) {
Label("Export", systemImage: "square.and.arrow.up")
}
}
}.fileExporter(isPresented: $showingExporter, document: createCSV(), contentType: .plainText) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
private func createCSV() -> TextFile {
print("CreateCSV")
}
I found that whenever my view dismiss, it will call createCSV() once.
SwiftUI uses a declarative syntax so whenever your state changes iOS will redraw the views. In your case when the views redraw, your function createCSV will be called because its return value is an argument for the modifier fileExporter.
To fix this use a state variable for your document, pass it as an argument for fileExporter and toggle the value only when the document is ready.
Modify your code to
#State private var showingExporter: Bool = false
#State private var document: TextFile?
var body: some View {
NavigationView {
VStack {
List {
Button(action: {
document = createCSV()
self.showingExporter.toggle()
}) {
Label("Export", systemImage: "square.and.arrow.up")
}
}
}
.fileExporter(isPresented: $showingExporter, document: document, contentType: .plainText) { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
private func createCSV() -> TextFile {
print("CreateCSV")
// Return your text file
return TextFile()
}

Proper way of Navigation in SwiftUI after async task

I have screen with List of Views one of them is NavigationLink that navigates to separate screen DocumentPreviewView where PDF document is presented.
This PDF document need to be downloaded asynchronously on the first screen after button is tapped and need to be passed to the DocumentPreviewView screen.
I came to working solution but I' m looking for more elegant way and more SwiftUI way.
Maybe document should be some kind of observable object.
This is my current solution. As you can see I use hidden NavigationLink that is being triggered by callback from button action where I also download document which I need to present.
#State private var document = PDFDocument()
#State private var docState: DocState? = .setup
enum DocState: Int {
case setup = 0
case ready = 1
}
var body: some View {
List {
/// some other views
Button(action: {
someAsyncFunction { doc, error in
self.document = doc
self.docState = .ready
}
}) {
Text("Show Document")
}
/// some other views
}
NavigationLink(
destination: DocumentPreviewView(pdfDocument: document),
tag: .ready,
selection: $docState) {
EmptyView()
}
}
Possible solution
struct ContentView: View {
#State private var docState: DocState? = .setup
enum DocState: Int {
case setup = 0
case ready = 1
}
var body: some View {
NavigationView {
ZStack {
NavigationLink(
destination: Text("Preview"),
tag: .ready,
selection: $docState) {
Button(action: {
someTask() { isTaskDone in
docState = .ready
}
}) {
Text("Download")
}
}
}
}
}
func someTask(_ completion: #escaping ((Bool)->())) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(true)
}
}
}

How To Switch A To A View In SwiftUI? [duplicate]

I'm trying to figure out the correct way to conditionally include a view with swiftui. I wasn't able to use the if directly inside of a view and had to use a
stack view to do it.
This works but there seems like there would be a cleaner way.
var body: some View {
HStack() {
if keychain.get("api-key") != nil {
TabView()
} else {
LoginView()
}
}
}
The simplest way to avoid using an extra container like HStack is to annotate your body property as #ViewBuilder, like this:
#ViewBuilder
var body: some View {
if user.isLoggedIn {
MainView()
} else {
LoginView()
}
}
I needed to embed a view inside another conditionally, so I ended up creating a convenience if function:
extension View {
#ViewBuilder
func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
if conditional {
content(self)
} else {
self
}
}
}
This does return an AnyView, which is not ideal but feels like it is technically correct because you don't really know the result of this during compile time.
In my case, I needed to embed the view inside a ScrollView, so it looks like this:
var body: some View {
VStack() {
Text("Line 1")
Text("Line 2")
}
.if(someCondition) { content in
ScrollView(.vertical) { content }
}
}
But you could also use it to conditionally apply modifiers too:
var body: some View {
Text("Some text")
.if(someCondition) { content in
content.foregroundColor(.red)
}
}
UPDATE: Please read the drawbacks of using conditional modifiers before using this: https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/
You didn't include it in your question but I guess the error you're getting when going without the stack is the following?
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
The error gives you a good hint of what's going on but in order to understand it, you need to understand the concept of opaque return types. That's how you call the types prefixed with the some keyword. I didn't see any Apple engineers going deep into that subject at WWDC (maybe I missed the respective talk?), which is why I did a lot of research myself and wrote an article on how these types work and why they are used as return types in SwiftUI.
🔗 What’s this “some” in SwiftUI?
There is also a detailed technical explanation in another
🔗 Stackoverflow post on opaque result types
If you want to fully understand what's going on I recommend reading both.
As a quick explanation here:
General Rule:
Functions or properties with an opaque result type (some Type)
must always return the same concrete type.
In your example, your body property returns a different type, depending on the condition:
var body: some View {
if someConditionIsTrue {
TabView()
} else {
LoginView()
}
}
If someConditionIsTrue, it would return a TabView, otherwise a LoginView. This violates the rule which is why the compiler complains.
If you wrap your condition in a stack view, the stack view will include the concrete types of both conditional branches in its own generic type:
HStack<ConditionalContent<TabView, LoginView>>
As a consequence, no matter which view is actually returned, the result type of the stack will always be the same and hence the compiler won't complain.
💡 Supplemental:
There is actually a view component SwiftUI provides specifically for this use case and it's actually what stacks use internally as you can see in the example above:
ConditionalContent
It has the following generic type, with the generic placeholder automatically being inferred from your implementation:
ConditionalContent<TrueContent, FalseContent>
I recommend using that view container rather that a stack because it makes its purpose semantically clear to other developers.
Anyway, the issue still exists.
Thinking mvvm-like all examples on that page breaks it.
Logic of UI contains in View.
In all cases is not possible to write unit-test to cover logic.
PS. I am still can't solve this.
UPDATE
I am ended with solution,
View file:
import SwiftUI
struct RootView: View {
#ObservedObject var viewModel: RatesListViewModel
var body: some View {
viewModel.makeView()
}
}
extension RatesListViewModel {
func makeView() -> AnyView {
if isShowingEmpty {
return AnyView(EmptyListView().environmentObject(self))
} else {
return AnyView(RatesListView().environmentObject(self))
}
}
}
Based on the comments I ended up going with this solution that will regenerate the view when the api key changes by using #EnvironmentObject.
UserData.swift
import SwiftUI
import Combine
import KeychainSwift
final class UserData: BindableObject {
let didChange = PassthroughSubject<UserData, Never>()
let keychain = KeychainSwift()
var apiKey : String? {
get {
keychain.get("api-key")
}
set {
if let newApiKey : String = newValue {
keychain.set(newApiKey, forKey: "api-key")
} else {
keychain.delete("api-key")
}
didChange.send(self)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView : View {
#EnvironmentObject var userData: UserData
var body: some View {
Group() {
if userData.apiKey != nil {
TabView()
} else {
LoginView()
}
}
}
}
Another approach using ViewBuilder (which relies on the mentioned ConditionalContent)
buildEither + optional
import PlaygroundSupport
import SwiftUI
var isOn: Bool?
struct TurnedOnView: View {
var body: some View {
Image(systemName: "circle.fill")
}
}
struct TurnedOffView: View {
var body: some View {
Image(systemName: "circle")
}
}
struct ContentView: View {
var body: some View {
ViewBuilder.buildBlock(
isOn == true ?
ViewBuilder.buildEither(first: TurnedOnView()) :
ViewBuilder.buildEither(second: TurnedOffView())
)
}
}
let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView
(There's also buildIf, but I couldn't figure out its syntax yet. ¯\_(ツ)_/¯)
One could also wrap the result View into AnyView
import PlaygroundSupport
import SwiftUI
let isOn: Bool = false
struct TurnedOnView: View {
var body: some View {
Image(systemName: "circle.fill")
}
}
struct TurnedOffView: View {
var body: some View {
Image(systemName: "circle")
}
}
struct ContentView: View {
var body: AnyView {
isOn ?
AnyView(TurnedOnView()) :
AnyView(TurnedOffView())
}
}
let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView
But it kinda feels wrong...
Both examples produce the same result:
I chose to solve this by creating a modifier that makes a view "visible" or "invisible". The implementation looks like the following:
import Foundation
import SwiftUI
public extension View {
/**
Returns a view that is visible or not visible based on `isVisible`.
*/
func visible(_ isVisible: Bool) -> some View {
modifier(VisibleModifier(isVisible: isVisible))
}
}
fileprivate struct VisibleModifier: ViewModifier {
let isVisible: Bool
func body(content: Content) -> some View {
Group {
if isVisible {
content
} else {
EmptyView()
}
}
}
}
Then to use it to solve your example, you would simply invert the isVisible value as seen here:
var body: some View {
HStack() {
TabView().visible(keychain.get("api-key") != nil)
LoginView().visible(keychain.get("api-key") == nil)
}
}
I have considered wrapping this into some kind of an "If" view that would
take two views, one when the condition is true and one when the condition is
false, but I decided that my present solution is both more general and more
readable.
Extension with the condition param works well for me (iOS 14):
import SwiftUI
extension View {
func showIf(condition: Bool) -> AnyView {
if condition {
return AnyView(self)
}
else {
return AnyView(EmptyView())
}
}
}
Example usage:
ScrollView { ... }.showIf(condition: shouldShow)
If the error message is
Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
Just hide the complexity of the control flow from the ViewBuilder:
This works:
struct TestView: View {
func hiddenComplexControlflowExpression() -> Bool {
// complex condition goes here, like "if let" or "switch"
return true
}
var body: some View {
HStack() {
if hiddenComplexControlflowExpression() {
Text("Hello")
} else {
Image("test")
}
if hiddenComplexControlflowExpression() {
Text("Without else")
}
}
}
}
Previous answers were correct, however, I would like to mention, you may use optional views inside you HStacks. Lets say you have an optional data eg. the users address. You may insert the following code:
// works!!
userViewModel.user.address.map { Text($0) }
Instead of the other approach:
// same logic, won't work
if let address = userViewModel.user.address {
Text(address)
}
Since it would return an Optional text, the framework handles it fine. This also means, using an expression instead of the if statement is also fine, like:
// works!!!
keychain.get("api-key") != nil ? TabView() : LoginView()
In your case, the two can be combined:
keychain.get("api-key").map { _ in TabView() } ?? LoginView()
Using beta 4
I extended #gabriellanata's answer for up to two conditions. You can add more if needed. You use it like this:
Text("Hello")
.if(0 == 1) { $0 + Text("World") }
.elseIf(let: Int("!")?.description) { $0 + Text($1) }
.else { $0.bold() }
The code:
extension View {
func `if`<TrueContent>(_ condition: Bool, #ViewBuilder transform: #escaping (Self) -> TrueContent)
-> ConditionalWrapper1<Self, TrueContent> where TrueContent: View {
ConditionalWrapper1<Self, TrueContent>(content: { self },
conditional: Conditional<Self, TrueContent>(condition: condition,
transform: transform))
}
func `if`<TrueContent: View, Item>(`let` item: Item?, #ViewBuilder transform: #escaping (Self, Item) -> TrueContent)
-> ConditionalWrapper1<Self, TrueContent> {
if let item = item {
return self.if(true, transform: {
transform($0, item)
})
} else {
return self.if(false, transform: {
transform($0, item!)
})
}
}
}
struct Conditional<Content: View, Trans: View> {
let condition: Bool
let transform: (Content) -> Trans
}
struct ConditionalWrapper1<Content: View, Trans1: View>: View {
var content: () -> Content
var conditional: Conditional<Content, Trans1>
func elseIf<Trans2: View>(_ condition: Bool, #ViewBuilder transform: #escaping (Content) -> Trans2)
-> ConditionalWrapper2<Content, Trans1, Trans2> {
ConditionalWrapper2(content: content,
conditionals: (conditional,
Conditional(condition: condition,
transform: transform)))
}
func elseIf<Trans2: View, Item>(`let` item: Item?, #ViewBuilder transform: #escaping (Content, Item) -> Trans2)
-> ConditionalWrapper2<Content, Trans1, Trans2> {
let optionalConditional: Conditional<Content, Trans2>
if let item = item {
optionalConditional = Conditional(condition: true) {
transform($0, item)
}
} else {
optionalConditional = Conditional(condition: false) {
transform($0, item!)
}
}
return ConditionalWrapper2(content: content,
conditionals: (conditional, optionalConditional))
}
func `else`<ElseContent: View>(#ViewBuilder elseTransform: #escaping (Content) -> ElseContent)
-> ConditionalWrapper2<Content, Trans1, ElseContent> {
ConditionalWrapper2(content: content,
conditionals: (conditional,
Conditional(condition: !conditional.condition,
transform: elseTransform)))
}
var body: some View {
Group {
if conditional.condition {
conditional.transform(content())
} else {
content()
}
}
}
}
struct ConditionalWrapper2<Content: View, Trans1: View, Trans2: View>: View {
var content: () -> Content
var conditionals: (Conditional<Content, Trans1>, Conditional<Content, Trans2>)
func `else`<ElseContent: View>(#ViewBuilder elseTransform: (Content) -> ElseContent) -> some View {
Group {
if conditionals.0.condition {
conditionals.0.transform(content())
} else if conditionals.1.condition {
conditionals.1.transform(content())
} else {
elseTransform(content())
}
}
}
var body: some View {
self.else { $0 }
}
}
How about that?
I have a conditional contentView, which either is a text or an icon. I solved the problem like this. Comments are very appreciated, since I don't know if this is really "swifty" or just a "hack", but it works:
private var contentView : some View {
switch kind {
case .text(let text):
let textView = Text(text)
.font(.body)
.minimumScaleFactor(0.5)
.padding(8)
.frame(height: contentViewHeight)
return AnyView(textView)
case .icon(let iconName):
let iconView = Image(systemName: iconName)
.font(.title)
.frame(height: contentViewHeight)
return AnyView(iconView)
}
}
Use Group instead of HStack
var body: some View {
Group {
if keychain.get("api-key") != nil {
TabView()
} else {
LoginView()
}
}
}
Here’s a very simple to use modifier which uses a boolean test to decide if a view will be rendered. Unlike other solutions posted here it doesn’t rely on the use of ÀnyView. This is how to use it:
var body: some View {
VStack {
FooView()
.onlyIf(someCondition)
}
}
This reads nicer than the default if … then construct as it removes the additional indentation.
To replace an if … then … else construct, this is the obvious solution:
var body: some View {
VStack {
FooView()
.onlyIf(someCondition)
BarView()
.onlyIf(!someCondition)
}
}
This is the definition of the onlyIf modifier:
struct OnlyIfModifier: ViewModifier {
var condition: Bool
func body(content: Content) -> some View {
if condition {
content
}
}
}
extension View {
func onlyIf(_ condition: Bool) -> some View {
modifier(OnlyIfModifier(condition: condition))
}
}
Give it a try – it will surely clean up your code and improve overall readability.
If you want to navigate to two different views using NavigationLink, you can navigate using ternary operator.
let profileView = ProfileView()
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
let otherProfileView = OtherProfileView(data: user)
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
NavigationLink(destination: profileViewModel.userName == user.userName ? AnyView(profileView) : AnyView(otherProfileView)) {
HStack {
Text("Navigate")
}
}

SwiftUI sheet not dismissing when isPresented value changes from a closure

I have a sheet view that is presented when a user clicks a button as shown in the parent view below:
struct ViewWithSheet: View {
#State var showingSheetView: Bool = false
#EnvironmetObject var store: DataStore()
var body: some View {
NavigationView() {
ZStack {
Button(action: { self.showingSheetView = true }) {
Text("Show sheet view")
}
}
.navigationBarHidden(true)
.navigationBarTitle("")
.sheet(isPresented: $showingSheetView) {
SheetView(showingSheetView: self.$showingSheetView).environmentObject(self.dataStore)
}
}
}
}
In the sheet view, when a user clicks another button, an action is performed by the store that has a completion handler. The completion handler returns an object value, and if that value exists, should dismiss the SheetView.
struct SheetView: View {
#Binding var showingSheetView: Bool
#EnvironmentObject var store: DataStore()
//#Environment(\.presentationMode) private var presentationMode
func create() {
store.createObject() { object, error in
if let _ = object {
self.showingSheetView = false
// self.presentationMode.wrappedValue.dismiss()
}
}
}
var body: some View {
VStack {
VStack {
HStack {
Button(action: { self.showingSheetView = false }) {
Text("Cancel")
}
Spacer()
Spacer()
Button(action: { self.create() }) {
Text("Add")
}
}
.padding()
}
}
}
}
However, in the create() function, once the store returns values and showingSheetView is set to false, the sheet view doesn't dismiss as expected. I've tried using presentationMode to dismiss the sheet as well, but this also doesn't appear to work.
I found my issue, the sheet wasn't dismissing due to a conditional in my overall App wrapping View, I had an if statement that would show a loading view on app startup, however, in my DataStore I was setting it's fetching variable on every function call it performs. When that value changed, the view stack behind my sheet view would re-render the LoadingView and then my TabView once the fetching variable changed again. This was making the sheet view un-dismissable. Here's an example of what my AppView looked like:
struct AppView: View {
#State private var fetchMessage: String = ""
#EnvironmentObject var store: DataStore()
func initializeApp() {
self.fetchMessage = "Getting App Data"
store.getData() { object, error in
if let error = error {
self.fetchMessage = error.localizedDescription
}
self.fetchMessage = ""
}
}
var body: some View {
Group {
ZStack {
//this is where my issue was occurring
if(!store.fetching) {
TabView {
Tab1().tabItem {
Image(systemName: "tab-1")
Text("Tab1")
}
Tab2().tabItem {
Image(systemName: "tab-2")
Text("Tab2")
}
//Tab 3 contained my ViewWithSheet() and SheetView()
Tab3().tabItem {
Image(systemName: "tab-3")
Text("Tab3")
}
}
} else {
LoadingView(loadingMessage: $fetchMessage)
}
}
}.onAppear(perform: initializeApp)
}
}
To solve my issue, I added another variable to my DataStore called initializing, which I use to render the loading screen or the actual application views on first .onAppear event in my app. Below is an example of what my updated AppView looks like:
struct AppView: View {
#State private var fetchMessage: String = ""
#EnvironmentObject var store: DataStore()
func initializeApp() {
self.fetchMessage = "Getting App Data"
store.getData() { object, error in
if let error = error {
self.fetchMessage = error.localizedDescription
}
self.fetchMessage = ""
//set the value to false once I'm done getting my app's initial data.
self.store.initializing = false
}
}
var body: some View {
Group {
ZStack {
//now using initializing instead
if(!store.initializing) {
TabView {
Tab1().tabItem {
Image(systemName: "tab-1")
Text("Tab1")
}
Tab2().tabItem {
Image(systemName: "tab-2")
Text("Tab2")
}
//Tab 3 contained my ViewWithSheet() and SheetView()
Tab3().tabItem {
Image(systemName: "tab-3")
Text("Tab3")
}
}
} else {
LoadingView(loadingMessage: $fetchMessage)
}
}
}.onAppear(perform: initializeApp)
}
}
Try to do this on main queue explicitly
func create() {
store.createObject() { object, error in
if let _ = object {
DispatchQueue.main.async {
self.showingSheetView = false
}
}
// think also about feedback on else case as well !!
}
}
Want to see something hacky that worked for me? Disclaimer: Might not work for you and I don't necessarily recommend it. But maybe it'll help someone in a pinch.
If you add a NavigationLink AND keep your fullScreenCover, then the fullscreen cover will be able to dismiss itself like you expect.
Why does this happen when you add the NavigationLink to your View? I don't know. My guess is it creates an extra reference somewhere.
Add this to your body, and keep your sheet as it is:
NavigationLink(destination: YOURVIEW().environmentObjects(), isActive: $showingSheetView) {}

Resources