Use of .fileExporter - ios

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

Related

PhotosPicker inside ForEach get called multiple times onChange

I'm building an app to let user create section and upload photos to each section. I have PhotosPicker() inside ForEach sections array
ForEach(sections,id: \.self){ section in
PhotosPicker(
selection: $selectedItem,
matching: .images,
photoLibrary: .shared()) {
...
}
.onChange(of: selectedItem) { newItem in
//This get called multiple times (for each section) even tap on one
}
}
So the idea is that the user can upload a photo to each section inside the for each. But when I do it like this, it triggers the photo picker for each section and ends up uploading the same photo to the same section.
Is there any way to make make the PhotosPicker call once?
Thank you so much in advance for your help!
Each section needs its own selectedItem so you have to put the PhotosPicker in a subview.
import PhotosUI
#available(iOS 16.0, *)
struct ReusablePhotoPickerView: View{
//Each row in the ForEach needs its own `#State` right now you are sharing one variable for all the sections.
#State private var selectedItem: PhotosPickerItem?
//Pass the UIImage to the section
let onSelected: (Result<UIImage, Error>) -> Void
var body: some View{
PhotosPicker(
selection: $selectedItem,
matching: .images,
photoLibrary: .shared()) {
Image(systemName: "plus.app")
}.task(id: selectedItem, {
do{
let result = try await selectedItem?.getImage()
switch result {
case .success(let success):
onSelected(.success(success))
case .failure(let failure):
onSelected(.failure(failure))
case .none:
onSelected(.failure(AppError.noImageFound))
}
}catch{
onSelected(.failure(error))
}
})
}
}
#available(iOS 16.0, *)
extension PhotosPickerItem{
func getImage() async throws -> Result<UIImage, Error>{
let data = try await self.loadTransferable(type: Data.self)
guard let data = data, let image = UIImage(data: data) else{
return .failure(AppError.noImageFound)
}
return .success(image)
}
}
enum AppError: LocalizedError{
case noImageFound
}
Once you have decoupled the section process you can assign the UIImage to the specific section.
import SwiftUI
#available(iOS 16.0, *)
struct MultiPhotoPicker: View {
#State var sections: [ImageModel] = [.init(description: "flowers"), .init(description: "trees")]
var body: some View {
VStack{
ForEach($sections) { $section in
Text(section.description)
HStack{
Spacer()
ForEach($section.images, id:\.cgImage) { $image in
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(height: 40)
}
Spacer()
ReusablePhotoPickerView { result in
switch result {
case .success(let success):
//Adding the image to the section.images array
$section.wrappedValue.images.append(success)
case .failure(let failure):
//You should always show errors to the user.
print("Error while adding images to \(section.description) \n\(failure)")
}
}
}
}
}
}
struct ImageModel: Identifiable{
let id: UUID = .init()
var images: [UIImage] = []
var description: String
}
}

List with editmode and onAppear

I am trying to display the list of items in editmode after the api call but List does not enter editmode. As a result, regular list of items is shown (not able to select an item). When I have static values, everything works well.
The view:
struct Step1: View {
#ObservedObject var page: Page
var body: some View {
VStack {
List(page.items, id: \.id, selection: $page.selectedItemStr) { item in
Text(item.name ?? "")
}
.listStyle(.plain)
.environment(\.editMode, $page.editMode)
.onAppear {
page.getItemsAsync()
}
}
}
}
editMode variable:
#Published var editMode: EditMode = .inactive
fetching data from api
func getItemsAsync() {
ItemService.getItemsAsyncMain() { (result) in
DispatchQueue.main.sync {
switch result {
case .success(let succes):
self.items = succes.results
self.hasMore = succes.hasMore
self.editMode = .active
case .failure(let failure):
self.items = []
print(failure)
}
}
}
}
I would appreciate any suggestions on why my code does not work. Thank you in advance

Handling asynchronous view controller retrieval in SwiftUI

I have a flow in UIKit where, when I call a function, I retrieve a response from my API, this response is then used to create a UIViewController. Then this view controller is presented full screen:
getResponse() { result in
switch result {
case .success(let response):
let viewController = MyViewController(response: response)
presenter.present(viewController) { success in
// etc
}
case .failure:
break
}
}
I want to implement the equivalent of this in SwiftUI using a view modifier to abstract this:
.showMyView(isPresented: $isPresented)
When isPresented is true, it should get the response then present MyView. I have bits in my head of what I think I need to use, but am not sure how to piece them together.
I know that I need to use a UIViewControllerRepresentable to handle the creation of a SwiftUI version of MyViewController:
struct MyView: UIViewControllerRepresentable {
var response: MyResponse
func makeUIViewController(context: Context) -> MyViewController {
return MyViewController(response: response)
}
}
I'm assuming I can pass the response in. However, how do I handle the asynchronousity of it with respect to the body of the view modifier?
struct ShowMyView: ViewModifier {
#Binding var isPresented: Bool
func body(content: Content) -> some View {
content
.sheet(isPresented: $isPresented) {
// something that returns MyView here?
}
.onReceive(Just(isPresented)) { _ in
getMyResponse()
}
}
func getMyResponse() {
getResponse() { result in
switch result {
case .success(let response):
// Something needs to happen here
case .failure:
break
}
}
}
}
I couldn't figure it out. Any help appreciated!
I think this is what you want:
struct ShowMyView: ViewModifier {
#Binding var shouldPresent: Bool
#State private var response: MyResponse? = nil
private var isPresentedBinding: Binding<Bool> {
return Binding(
get: { shouldPresent && response != nil },
set: { shouldPresent = $0 }
)
}
func body(content: Content) -> some View {
content
.sheet(isPresented: isPresentedBinding) {
if let response = response {
MyView(response: response)
}
}
.onChange(of: shouldPresent) {
if $0 && response == nil {
getMyResponse()
}
}
}
func getMyResponse() {
getResponse() { result in
switch result {
case .success(let response):
self.response = response
case .failure:
break
}
}
}
}
You need internal state, like (typed here, so some typos/errors might needed to be fixed)
struct ShowMyView: ViewModifier {
#Binding var isPresented: Bool
#State private var result: MyResponse? = nil
func body(content: Content) -> some View {
content
.sheet(item: $result) { // MyResponse needed to be Identifiable
switch $0 {
case .success(let response):
MyView(response: response)
case .failure:
// something appropriate here, e.g. ErrorView()
break
}
}
.onReceive(Just(isPresented)) { _ in
getMyResponse()
}
}
func getMyResponse() {
getResponse() { result in
DispatchQueue.main.async {
self.result = result // update UI on main queue
}
}
}
}

Show ProgressView when Exporting File in SwiftUI

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

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

Resources