Using #Environment PresentationMode in SwiftUI View and UIViewControllerRepresentable Causing Problems - swiftui

I am working on a SwiftUI project that is using a UIImagePickerController through UIViewControllerRepresentable. In the UIViewControllerRepresentable file I add the following line so that I can dismiss the ImagePicker in imagePickerController didFinishPickingMediaWithOptions method.
#Environment(\.presentationMode) var presentationMode
In my view file I would also like to be able to dismiss the view so I add the line.
#Environment(\.presentationMode) var presentaionMode: Binding<PresentationMode>
However, when this line is added to the view, it causes the ActionSheet to be created four time. Once an item is selected from the ActionSheet it is called twice more. The print statement ShowSheetButtons is how I've identified it being called multiple times. If this #Environment line is removed, all performs as expected. Is there a reason this is happening and how can I fix it. I'd like to be able to use presentationMode in both the View and the UIViewControllerRpresentable.
View
struct CreateListingView: View {
// MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
// Dismiss the View
#Environment(\.presentationMode) var presentaionMode: Binding<PresentationMode>
// Media Picker
#State var showMediaPickerSheet = false
#State var showLibrary = false
#State var showMediaErrorAlert = false
#frozen enum MediaTypeSource {
case library
}
// MARK: ++++++++++++++++++++++++++++++++++++++ View ++++++++++++++++++++++++++++++++++++++
var body: some View {
return
Button(action: {
self.presentMediaPicker()
}, label: {
Text("Button")
})
.actionSheet(isPresented: $showMediaPickerSheet, content: {
ActionSheet(
title: Text("Add Store Picture"),
buttons: sheetButtons()
)
}) // Action Sheet
.sheet(isPresented: $showLibrary, content: {
MediaPickerPhoto(sourceType: .photoLibrary, showError: $showMediaErrorAlert) { (image, error) in
if error != nil {
print(error!)
} else {
guard let image = image else {
return
}
}
}
})
} // View
// MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
// Media Picker Methods
func presentMediaPicker() {
print("PresentMediaPicker1Listing")
self.showMediaPickerSheet = true
}
func sheetButtons() -> [Alert.Button] {
print("ShowSheetButtonsListing")
return UIImagePickerController.isSourceTypeAvailable(.camera) ? [
.default(Text("Choose Photo")) {
presentMediaPicker1(.library)
},
.cancel {
showMediaPickerSheet = false
}
] : [
.default(Text("Choose Photo")) {
presentMediaPicker1(.library)
},
.cancel {
showMediaPickerSheet = false
}
]
}
private func presentMediaPicker1(_ type: MediaTypeSource) {
print("PresentMediaPicker2Listing")
showMediaPickerSheet = false
switch type {
case .library:
showLibrary = true
}
}
}
UIViewControllerRepresentable
struct MediaPickerPhoto: UIViewControllerRepresentable {
typealias UIViewControllerType = UIImagePickerController
/// Presentation wrapper
#Environment(\.presentationMode) var presentationMode
/// Source type to present for
let sourceType: UIImagePickerController.SourceType
/// Binding for showing error alerts
#Binding var showError: Bool
/// Callback for media selection
let completion: (UIImage?, String?) -> Void
// MARK: - Representable
func makeUIViewController(context: UIViewControllerRepresentableContext<MediaPickerPhoto>) -> UIImagePickerController {
print("MakeUIViewController")
let picker = UIImagePickerController()
if sourceType == .camera {
picker.videoQuality = .typeMedium
}
picker.sourceType = sourceType
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<MediaPickerPhoto>) {
// no-op
}
// MARK: - Coordinator
func makeCoordinator() -> MediaCoordinatorPhoto {
return Coordinator(self)
}
}
/// Coordinator for media picker
class MediaCoordinatorPhoto: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
// AuthSession
#EnvironmentObject var authSession: AuthSession
let db = Firestore.firestore()
/// Parent picker
let parent: MediaPickerPhoto
// MARK: - Init
init(_ parent: MediaPickerPhoto) {
print("MediaCoordinatorPhoto")
self.parent = parent
}
// MARK: - Delegate
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let type = (info[.mediaType] as? String)?.lowercased() else {
return
}
if type.contains("image"), let uiImage = info[.originalImage] as? UIImage {
// We attempt to resize the image to max os 1280x1280 for perf
// If it fails, we use the original selection image
let image = uiImage.resized(maxSize: CGSize(width: 1280, height: 1280)) ?? uiImage
self.parent.completion(image, nil)
} else {
print("Invalid media type selected")
let error = "There was an error updating your user profile picture. Please try again later."
parent.completion(nil, error)
//parent.showError = true
}
parent.presentationMode.wrappedValue.dismiss()
}
}
Edited
2021-11-03 17:20:13.799510-0700 Global Store Exchange[6900:1991500] [lifecycle] [u
478AD6F6-A9E2-4FE9-96D0-D310B754DD59:m (null)]
[com.apple.mobileslideshow.photo-picker(1.0)] Connection to plugin
interrupted while in use.
2021-11-03 17:20:13.800173-0700 Global Store Exchange[6900:1990765] viewServiceDidTerminateWithError:: Error
Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)"
UserInfo={Message=Service Connection Interrupted}
2021-11-03 17:20:13.800383-0700 Global Store Exchange[6900:1990765] [UI] -[PUPhotoPickerHostViewController
viewServiceDidTerminateWithError:] Error Error
Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)"
UserInfo={Message=Service Connection Interrupted}
2021-11-03 17:20:13.800987-0700 Global Store Exchange[6900:1990765] viewServiceDidTerminateWithError:: Error
Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)"
UserInfo={Message=Service Connection Interrupted}
2021-11-03 17:20:13.801016-0700 Global Store Exchange[6900:1990765] [UI] -[PUPhotoPickerHostViewController
viewServiceDidTerminateWithError:] Error Error
Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)"
UserInfo={Message=Service Connection Interrupted}
2021-11-03 17:20:13.801089-0700 Global Store Exchange[6900:1990765] UIImagePickerController UIViewController create
error: Error Domain=NSCocoaErrorDomain Code=4097 "connection to
service named com.apple.mobileslideshow.photo-picker.viewservice"
UserInfo={NSDebugDescription=connection to service named
com.apple.mobileslideshow.photo-picker.viewservice}
2021-11-03 17:20:13.801266-0700 Global Store Exchange[6900:1990765] viewServiceDidTerminateWithError:: Error
Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)"
UserInfo={Message=Service Connection Interrupted}
2021-11-03 17:20:13.801313-0700 Global Store Exchange[6900:1990765] [UI] -[PUPhotoPickerHostViewController
viewServiceDidTerminateWithError:] Error Error
Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)"
UserInfo={Message=Service Connection Interrupted}
2021-11-03 17:20:13.801421-0700 Global Store Exchange[6900:1990765] viewServiceDidTerminateWithError:: Error
Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)"
UserInfo={Message=Service Connection Interrupted}
2021-11-03 17:20:13.801459-0700 Global Store Exchange[6900:1990765] [UI] -[PUPhotoPickerHostViewController
viewServiceDidTerminateWithError:] Error Error
Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)"
UserInfo={Message=Service Connection Interrupted}

I figured out a work around to this issue. Instead of using #EnvironmentObject I am using a Bool and the isActive property in NavigationLink.
In the view that presents the CreateListingView I add a Toolbar to the NavigationView. In the ToolbarItem I toggle an #State variable that is connected to the isActive property.
struct MarketplaceView: View {
#State private var presentCreateListingView = false
#EnvironmentObject var marketplaceViewModel: MarketplaceViewModel
var body: some View {
NavigationView {
List {
ForEach(self.marketplaceViewModel.filteredListingRowViewModels, id: \.id) { listingRowViewModel in
NavigationLink(destination: ListingDetailView()) {
ListingRowView(listingRowViewModel: listingRowViewModel)
}
} // ForEach
} // List
.navigationTitle("Marketplace")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
NavigationLink(destination: CreateListingView(presentCreateListingView: self.$presentCreateListingView), isActive: self.$presentCreateListingView, label: {
Button(action: {
self.presentCreateListingView.toggle()
}, label: {
Image(systemName: "plus")
})
}) // NavigationLink
} // ToolbarItem
} // Toolbar
} // NavigationView
//.navigationViewStyle(StackNavigationViewStyle())
} // View
}
Then in the CreateListingView I have an #Binding to the Bool. Then I use a Button to toggle the Bool.
struct CreateListingView: View {
#Binding var presentCreateListingView: Bool
var body: some View {
Button(action: {
self.presentCreateListingView.toggle()
}, label: {
Text("Back")
})
} // View
}
I hope this helps someone out.

Related

SwiftUI UIIMagePickerController Find/Search Field Strange Behaviour

I'm using a UIImagePickerController to select an image from the user's photo library. All works fine except when the user taps on the field with the magnifying glass icon to search the library. Sometimes it dismisses ImagePicker, other times it does nothing and occasionally it works as expected... any ideas?
Code to display the sheet:
Button(action: {}) {
ZStack {
Image(systemName: "photo")
.foregroundColor(darkMode ? .white : .black)
}
}
.frame(width: 44, height: 44)
.onTapGesture {
showPhotoLibrary.toggle()
}
.sheet(isPresented: $showPhotoLibrary) {
ImagePicker(sourceType: .photoLibrary, selectedImage: self.$image)
}
The ImagePicker:
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var selectedImage: UIImage
#Environment(\.presentationMode) private var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let imagePicker = UIImagePickerController()
imagePicker.sourceType = sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
internal func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
parent.selectedImage = image
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
I've seen many struggle with this, it's all over the Apple dev forums so I thought I would have a go at solving it. From my own testing I believe the reason for the problems is UIImagePickerController is designed to be presented using the present method of a parent View Controller, not used as the root view controller of a SwiftUI presented sheet, as Apple (in their sample code) and everyone else seem to be doing. Below is my incomplete test code, which shows a working image picker where the search works and it can also be dragged down to dismiss, so I think this might be the correct solution.
The idea is we use UIViewControllerRepresentable to present our own custom view controller that presents or dismisses the image picker view controller when the present boolean changes.
struct ImagePickerTest: View {
#State var show = false
#State var image1: UIImage?
var body: some View {
VStack {
Button("Hello, World!") {
show.toggle()
}
Text("image: \(image1?.description ?? "" )")
}
.imagePicking(isPresented: $show, selectedImage: $image1)
}
}
extension View {
// this could be refactored into a `ViewModifier`
func imagePicking(isPresented: Binding<Bool>, selectedImage: Binding<UIImage?>) -> some View {
ZStack {
ImagePicking(isPresented: isPresented, selectedImage: selectedImage)
self
}
}
}
class MyViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate{
var dismissPicker: (() -> Void)?
var selectImage: ((UIImage) -> Void)?
func showPickerIfNecessary() {
if self.presentedViewController != nil {
return
}
let imagePickerController = UIImagePickerController()
imagePickerController.sourceType = .photoLibrary
imagePickerController.delegate = self
present(imagePickerController, animated: true)
}
func hidePickerIfNecessary() {
if let vc = self.presentedViewController {
vc.dismiss(animated: true)
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
print("imagePickerControllerDidCancel")
dismissPicker?()
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
print("didFinishPickingMediaWithInfo")
if let image = (info[.editedImage] ?? info[.originalImage]) as? UIImage {
selectImage?(image)
dismissPicker?()
}
}
}
struct ImagePicking: UIViewControllerRepresentable {
#Binding var isPresented: Bool
#Binding var selectedImage: UIImage?
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
uiViewController.dismissPicker = {
isPresented = false
}
uiViewController.selectImage = { image in
selectedImage = image
}
if isPresented {
uiViewController.showPickerIfNecessary()
} else {
uiViewController.hidePickerIfNecessary()
}
}
func makeUIViewController(context: Context) -> some MyViewController {
MyViewController()
}
}
Before I started, I verified the OP's code was failing when typing a search, it crashed with this error:
2022-08-31 22:39:17.808899+0100 Test[66967:5425868] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] Error Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}

Add a different modifier to each Action Sheet button

This is probably quite a simple question, but I can't find an answer.
I'm trying to build an ActionSheet with two buttons (as well as a cancel button):
Button "Select from Gallery" opens an imagePicker with sourceType set to .photoLibrary.
Button "Take a new picture" opens an imagePicker with sourceType set to .camera.
I've made the ActionSheet and the imagePicker successfully, but can't work out where to add the modifier to tell which sourceType should be used to each button. I managed to add it outside of the ActionSheet in a sheet() modifier in a normal button like this and everything worked well:
Button(action: {
self.show.toggle()
})
{Text("Take a new picture")}
.sheet(isPresented: self.$show, content: {
ImagePicker(sourceType: .camera, show: self.$show, image: self.$imageTemp)
})
However I can't see where to include this information in the ActionSheet. Many thanks to anyone who can help, I hope this is clear :-)
Here is my code:
struct ContentView: View {
#State private var showingActionSheet = false
#State var imageTemp : Data = (UIImage(systemName: "photo.on.rectangle.angled")?.jpegData(compressionQuality: 1))!
var body: some View {
NavigationView {
Image(uiImage: UIImage(data: imageTemp)!)
.onTapGesture {
self.showingActionSheet = true
}
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(title: Text("Image selector"), message: Text("Select an image"), buttons: [
.default(Text("Select from Gallery"))
{
self.show.toggle()
},
.default(Text("Take new picture")) {
self.show.toggle()
},
.cancel()
]
)
}
}
}
}
And, just in case, here is the code for my imagePicker, although I think it's probably not necessary.
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var show: Bool
#Binding var image: Data
func makeCoordinator() -> ImagePicker.Coordinator {
let imagePicker = UIImagePickerController()
return ImagePicker.Coordinator(child1: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = sourceType
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var child : ImagePicker
init(child1: ImagePicker) {
child = child1
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.child.show.toggle()
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
let image = info[.originalImage]as! UIImage
let data = image.jpegData(compressionQuality: 0.45)
self.child.image = data!
self.child.show.toggle()
}
}
}
Your question pretty much boils down to "How can I present multiple sheets?", so this thread might be helpful.
Define a new enum to contain possible sheet types (gallery/take photo)
Declare a #State property to hold the current sheet type. It's optional because when it's nil, there will be no sheet presented.
Set the property to the type that you want
Use sheet(item:onDismiss:content:) instead of sheet(isPresented:onDismiss:content:). isPresented is best for static sheets. item is for when you have multiple sheet types, which is what you want.
enum PhotoSheetType: Identifiable { /// 1.
var id: UUID {
UUID()
}
case gallery
case picture
}
struct ContentView: View {
/// 2.
#State private var showingType: PhotoSheetType?
#State private var showingActionSheet = false
#State var imageTemp : Data = (UIImage(systemName: "photo.on.rectangle.angled")?.jpegData(compressionQuality: 1))!
var body: some View {
NavigationView {
Image(uiImage: UIImage(data: imageTemp)!)
.onTapGesture {
self.showingActionSheet = true
}
.actionSheet(isPresented: $showingActionSheet) {
ActionSheet(
title: Text("Image selector"),
message: Text("Select an image"),
buttons: [
.default(Text("Select from Gallery")) {
showingType = .gallery /// 3.
},
.default(Text("Take new picture")) {
showingType = .picture /// 3.
},
.cancel()
]
)
} /// 4.
.sheet(item: $showingType) { type in
if type == .gallery {
ImagePicker(sourceType: .photoLibrary, showingType: $showingType, image: self.$imageTemp)
} else {
ImagePicker(sourceType: .camera, showingType: $showingType, image: self.$imageTemp)
}
}
}
}
}
You'll also need to modify your ImagePicker so that the Binding takes in a PhotoSheetType? instead of Bool. To dismiss the sheet, just set showingType to nil.
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var showingType: PhotoSheetType?
#Binding var image: Data
func makeCoordinator() -> ImagePicker.Coordinator {
let imagePicker = UIImagePickerController()
return ImagePicker.Coordinator(child1: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = sourceType
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var child : ImagePicker
init(child1: ImagePicker) {
child = child1
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.child.showingType = nil /// set to nil here
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
let image = info[.originalImage] as! UIImage
let data = image.jpegData(compressionQuality: 0.45)
self.child.image = data!
self.child.showingType = nil /// set to nil here
}
}
}

SwiftUI: #State property not updated without weird workaround

I'm experiencing strange behavior with an #State property that isn't being properly updated in its originating view after being changed in another view. I'm using Xcode 12.3 and iOS 14.
What happens is that an #State "session" value-based item and #State "flow" value-based item are sent as bound parameters to another view. When a button is tapped there, it changes their values, and a fullScreenCover call in the originating view is supposed to get the correct view to display next in the flow from a switch statement. But the "session" item is nil in that switch statement unless I include an onChange modifier that looks for changes in either of the two #State properties. The onChange call doesn't have to have any code in it to have this effect.
I'm still relatively new to SwiftUI (although fairly experienced with iOS and Mac development). But this is confusing the heck out of me. I don't understand why it isn't working as expected, nor why adding an empty onChange handler makes it work.
If you'd like to experience this for yourself, here's code to assemble a simple demo project:
// the model types
struct ObservationSession: Codable {
public let id: UUID
public var name: String
public init(name: String) {
self.name = name
self.id = UUID()
}
}
struct SessionListModals {
enum Flow: Identifiable {
case configuration
case observation
case newSession
var id: Flow { self }
}
}
// ContentView
struct ContentView: View {
#State private var mutableSession: ObservationSession?
#State private var flow: SessionListModals.Flow?
var body: some View {
VStack {
Button("New Session", action: {
mutableSession = ObservationSession(name: "")
flow = .newSession
})
.padding()
}
.fullScreenCover(item: $flow) {
viewForFlow($0)
}
// Uncomment either of these 2 onChange blocks to see successful execution of this flow
// Why does that make a difference?
// .onChange(of: mutableSession?.name, perform: { value in
// //
// })
// .onChange(of: flow, perform: { value in
// //
// })
}
#ViewBuilder private func viewForFlow(_ flow: SessionListModals.Flow) -> some View {
switch flow {
case .newSession:
// MARK: - Show New Session View
NavigationView {
NewSessionView(session: $mutableSession, flow: $flow)
.navigationTitle("Create a session")
.navigationBarItems(leading: Button("Cancel", action: {
self.flow = nil
}))
}
case .observation:
// MARK: - Show RecordingView
NavigationView {
let name = mutableSession?.name ?? "Unnamed session"
RecordingView(sessionName: name)
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
default:
NavigationView {
EmptyView()
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// NewSessionView
struct NewSessionView: View {
#Binding var session: ObservationSession?
#Binding var flow: SessionListModals.Flow?
var body: some View {
VStack {
Text("Tap button to create a new session")
Button("New Session", action: {
createNewSession()
})
.padding()
}
}
private func createNewSession() {
let newSession = ObservationSession(name: "Successfully Created A New Session")
session = newSession
flow = .observation
}
}
struct NewSessionView_Previews: PreviewProvider {
static let newSession = ObservationSession(name: "Preview")
static let flow: SessionListModals.Flow = .newSession
static var previews: some View {
NewSessionView(session: .constant(newSession), flow: .constant(flow))
}
}
// RecordingView
struct RecordingView: View {
var sessionName: String
var body: some View {
Text(sessionName)
}
}
struct RecordingView_Previews: PreviewProvider {
static var previews: some View {
RecordingView(sessionName: "Preview")
}
}
class ObservationSession: //Codable, //implement Codable manually
ObservableObject {
public let id: UUID
//This allows you to observe the individual variable
#Published public var name: String
public init(name: String) {
self.name = name
self.id = UUID()
}
}
struct SessionListModals {
enum Flow: Identifiable {
case configuration
case observation
case newSession
var id: Flow { self }
}
}
// ContentView
class ContentViewModel: ObservableObject {
#Published var mutableSession: ObservationSession?
}
struct ContentView: View {
//State stores the entire object and observes it as a whole it does not individually observe its variables that is why .onChange works
#StateObject var vm: ContentView3Model = ContentView3Model()
#State private var flow: SessionListModals.Flow?
var body: some View {
VStack {
Button("New Session", action: {
//Since you want to change it programatically you have to put them in another object
vm.mutableSession = ObservationSession(name: "")
flow = .newSession
})
.padding()
}
.fullScreenCover(item: $flow) {
viewForFlow($0)
}
}
#ViewBuilder private func viewForFlow(_ flow: SessionListModals.Flow) -> some View {
switch flow {
case .newSession:
// MARK: - Show New Session View
NavigationView {
NewSessionView(session: $vm.mutableSession, flow: $flow)
.navigationTitle("Create a session")
.navigationBarItems(leading: Button("Cancel", action: {
self.flow = nil
}))
}
case .observation:
// MARK: - Show RecordingView
NavigationView {
let name = vm.mutableSession?.name ?? "Unnamed session"
RecordingView(sessionName: name)
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
default:
NavigationView {
EmptyView()
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
}
}
}

How to reload a view screen when some values which generate by ForEach method are changed?

I'm currently developing an application using SwiftUI.
I'm trying to make a view to show some value as a list using CRUD API calls.
In the case of my codes, when I add or remove a number of arrays(lists) the view reloads a screen, but when I edit some data in an array(list) the view doesn't reload the screen with new values...
how could I resolve this problem?
Here are the codes:
HomeView.swift
import SwiftUI
struct HomeView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView{
VStack{
ForEach(appState.arrayInfos ?? []){ info in
VStack{
InfoRow(
id: info.id,
name: info.name,
memo: info.memo ?? "",
)
}
NavigationLink(destination: DetailView(),
isActive: $appState.isNavigateToDetailView){
EmptyView()
}
}
}
}.onAppear(){
appState.makeGetCallInfos()
}
}
}
InfoRow.swift
import SwiftUI
struct InfoRow: View {
#EnvironmentObject var appState: AppState
#State var id: Int
#State var name: String
#State var memo: String
var body: some View {
VStack{
Text(String(id))
Text(name)
Text(memo)
}
}
}
JsonModel.swift
import Foundation
struct Infos: Codable,Identifiable {
var id: Int
var name: String
var memo: String?
}
AppState.swift
import SwiftUI
import Foundation
import Combine
import UIKit
class AppState: ObservableObject {
#Published var isNavigateToDetailView:Bool = false
#Published var infos:Infos?
#Published var arrayInfos:[Infos]?
func makeGetCallInfos() {
let endpoint: String = "https://sample.com/api/info/"
guard let url = URL(string: endpoint) else {
print("Error: cannot create URL")
return
}
var urlRequest = URLRequest(url: url)
urlRequest.addValue("token xxxxxxxxxxxx", forHTTPHeaderField: "authorization")
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
DispatchQueue.main.async {
do{ self.arrayInfos = try JSONDecoder().decode([Infos].self,from:responseData)
}catch{
print("Error: did not decode")
return
}
}
}
task.resume()
}
I tried to change the code like this but then I have an error like below:
HomeView.swift
VStack{
ForEach(appState.arrayInfos ?? []){ info in
VStack{
InfoRow(
id: appState.infos!.id,
name: appState.infos!.name,
memo: appState.infos!.memo ?? "",
)
}
NavigationLink(destination: DetailView(),
isActive: $appState.isNavigateToDetailView){
EmptyView()
}
}
}
error message
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Xcode:Version 12.0.1
I could solve this problem to change the codes like below:
removing #State
InfoRow.swift
import SwiftUI
struct InfoRow: View {
#EnvironmentObject var appState: AppState
var id: Int
var name: String
var memo: String
var body: some View {
VStack{
Text(String(id))
Text(name)
Text(memo)
}
}
}

How to change #State property wrapper from nested view

I'm wondering how I should change #State property wrapper showErrorAlert in view below
struct SettingsView: View {
#State private var shouldPresent = false
#State var showErrorAlert = false
var body: some View {
VStack {
Form {
Text("Settings")
.font(.title)
Button("Import source data") {
self.shouldPresent.toggle()
}
.sheet(isPresented: $shouldPresent) {
DocumentPicker()
}
Button("Show error alert") {
self.showErrorAlert.toggle()
}
.alert(isPresented: $showErrorAlert, content: {
Alert(title: Text("Error"))
})
}
}
}
}
from DocumentPicker struct code in case that reading of selected file fails.
struct DocumentPicker: UIViewControllerRepresentable {
func makeCoordinator() -> DocumentPicker.Coordinator {
return DocumentPicker.Coordinator(parent: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPicker>) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(documentTypes: [String(kUTTypeJSON)], in: .import)
picker.allowsMultipleSelection = false
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext<DocumentPicker>) {
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var myParent: DocumentPicker
init(parent: DocumentPicker) {
myParent = parent
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let fileURL = urls.first!
do {
let origFile = try String(contentsOf: fileURL)
//File processing will be here
} catch let error {
print(error)
}
}
}
}
I mean how to set property wrapper value to true to show the alert. Should I rather use #ObservedObject or #EnvironmentObject instead?
Thanks.
To change the wrapper value in your DocumentPicker struct you can define a #Binding variable and pass your value to it, this toggle your variable on your parent view, but before showing the alert you need to dismiss the DocumentPicker