Change State with DocumentPicker SwiftUI - view

I'm trying to get a List View to appear after selecting a document with documentPicker. Getting the following error...
Fatal error: No ObservableObject of type Switcher found.
A View.environmentObject(_:) for Switcher may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros/Monoceros-30.4/Core/EnvironmentObject.swift, line 55
It seems like I should use an EnviromentObject binding to have all views be able to read, access and update the Switcher class. Under the Coordinator Class in CSVDocumentPicker.swift is where things seem to go wrong.
I'm using #EnvironmentObject var switcher:Switcher and using the documentPicker function to toggle the switcher state so the Lists View will be displayed. I'm stumped.
SceneDelegate.swift
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var switcher = Switcher()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView().environmentObject(Switcher())
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(switcher))
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
}
func sceneDidBecomeActive(_ scene: UIScene) {
}
func sceneWillResignActive(_ scene: UIScene) {
}
func sceneWillEnterForeground(_ scene: UIScene) {
}
func sceneDidEnterBackground(_ scene: UIScene) {
}
}
CSVDocumentPicker.swift
import Combine
import SwiftUI
class Switcher: ObservableObject {
var didChange = PassthroughSubject<Bool, Never>()
var isEnabled = false {
didSet {
didChange.send(self.isEnabled)
}
}
}
struct CSVDocumentPicker: View {
#EnvironmentObject var switcher:Switcher
#State private var isPresented = false
var body: some View {
VStack{
Text("csvSearch")
Button(action: {self.isPresented = true
})
{Text("import")
Image(systemName: "folder").scaledToFit()
}.sheet(isPresented: $isPresented) {
() -> DocumentPickerViewController in
DocumentPickerViewController.init(onDismiss: {
self.isPresented = false
})
}
if switcher.isEnabled {
ListView()
}
}
}
}
struct CSVDocumentPicker_Previews: PreviewProvider {
static var previews: some View {
CSVDocumentPicker().environmentObject(Switcher())
}
}
/// Wrapper around the `UIDocumentPickerViewController`.
struct DocumentPickerViewController {
private let supportedTypes: [String] = ["public.item"]
// Callback to be executed when users close the document picker.
private let onDismiss: () -> Void
init(onDismiss: #escaping () -> Void) {
self.onDismiss = onDismiss
}
}
// MARK: - UIViewControllerRepresentable
extension DocumentPickerViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = UIDocumentPickerViewController
func makeUIViewController(context: Context) -> DocumentPickerViewController.UIViewControllerType {
let documentPickerController = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
documentPickerController.allowsMultipleSelection = false
documentPickerController.delegate = context.coordinator
return documentPickerController
}
func updateUIViewController(_ uiViewController: DocumentPickerViewController.UIViewControllerType, context: Context) {}
// MARK: Coordinator
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIDocumentPickerDelegate, ObservableObject {
#EnvironmentObject var switcher:Switcher
var parent: DocumentPickerViewController
init(_ documentPickerController: DocumentPickerViewController) {
parent = documentPickerController
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
globalPathToCsv = url
loadCSV()
switcher.isEnabled.toggle()
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
parent.onDismiss()
}
}
}
ContentView.swift
import SwiftUI
import UIKit
var globalPathToCsv:URL!
var csvArray = [[String:String]]()
var csv = CSVaccessability()
func loadCSV(){
csv.csvToList()
// print(csvArray)
}
struct ContentView: View {
#EnvironmentObject var switcher:Switcher
var body: some View {
VStack{
CSVDocumentPicker().environmentObject(Switcher())
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Switcher())
}
}
ListView.swift
import SwiftUI
struct ListView: View {
var body: some View {
HStack{
List {
ForEach(csvArray, id:\.self) { dict in Section {DataList(dict: dict)} }
}
}}
}
struct DataList : View {
#State var dict = [String: String]()
var body: some View {
let keys = dict.map{$0.key}
let values = dict.map {$0.value}
return ForEach(keys.indices) {index in
HStack {
Text(keys[index])
Text("\(values[index])")
}
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
CSVaccessability.swift
import Foundation
import SwiftCSV
var csvData:[[String]]!
var headers:[String] = []
class CSVaccessability {
var numberOfColumns:Int!
var masterList = [[String:Any]]()
func csvToList(){
if let url = globalPathToCsv {
do {
print(url)
let csvFile: CSV = try CSV(url: globalPathToCsv)
let csv = csvFile
//print(stream)
//print(csvFile)
headers = csv.header
csvArray=csv.namedRows
} catch {print("contents could not be loaded")}}
else {print("the URL was bad!")}
}
}
I've imported SwiftCSV for this project...

Created a new class...
ToggleView.swift
import Foundation
class ToggleView: ObservableObject {
#Published var toggleView: Bool = false
}
Added the Environment Object to ContentView.swift
#EnvironmentObject var viewToggle: ToggleView
Also added .environmentObject(ToggleView()) to any view that would be called and cause a crash the crash logs helped with this...
Text("csvSearch")
Button(action: {self.isPresented = true
self.viewToggle.toggleView.toggle()
// self.switcher = true
})
{Text("import")
Image(systemName: "folder").scaledToFit()
}.sheet(isPresented: $isPresented) {
() -> DocumentPickerViewController in
DocumentPickerViewController.init()
}
if self.picker {
DocumentPickerViewController().environmentObject(ToggleView())
}
if self.viewToggle.toggleView{
ListView()
}
}
}
}

Did you ever get this working? The only problem I found was with the line var csv = CSVaccessability() in the ContentView. CSVaccessability does not exist.

This is my solution for the Catalyst app for Mac, but to avoid pressing theImage (systemName: "book")button a second time to update the data in the text fields, I have implemented a hidden view in GeoFolderReadFileView to force the view update.
//File: GeoFolderCodStruct
import Foundation
struct GeoFolderCodStruct:Codable {
var isActive:Bool = true
var dataCreazione:Date = Date()
var geoFolderPath:String = ""
var nomeCartella:String = ""
var nomeCommittente:String = ""
var nomeArchivio:String = ""
var note:String = ""
var latitudine:Double? = nil
var longitudine:Double? = nil
var radiusCircle:Int16? = nil
//Roma 42.1234 13.1234
}
//File: PickerForReadFile
import SwiftUI
final class PickerForReadFile: NSObject, UIViewControllerRepresentable, ObservableObject {
#Published var geoFolder = GeoFolderCodStruct()
lazy var viewController:UIDocumentPickerViewController = {
let vc = UIDocumentPickerViewController(documentTypes: ["geof"], in: .open)
vc.allowsMultipleSelection = false
vc.delegate = self
return vc
}()
func makeUIViewController(context: UIViewControllerRepresentableContext<PickerForReadFile>) -> UIDocumentPickerViewController {
viewController.delegate = self
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext<PickerForReadFile>) {
print("updateUIViewController")
}
}
extension PickerForReadFile: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
if urls.count > 0 {
DispatchQueue.main.async {
let url = urls[0]
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let jsonData = try decoder.decode(GeoFolderCodStruct.self, from: data)
self.geoFolder = jsonData
print("geoFolder: \(self.geoFolder.nomeArchivio)")
} catch {
print("error:\(error)")
}
}
}
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
controller.dismiss(animated: true) {
print("Cancel from picker view controller")
}
}
}
//File: GeoFolderReadFileView
import SwiftUI
struct GeoFolderReadFileView: View {
#ObservedObject var picker = PickerForReadFile()
#Binding var geoFolder: GeoFolderCodStruct
var body: some View {
VStack(alignment: .trailing){
Button(action: {
#if targetEnvironment(macCatalyst)
print("Press open file")
let vc = UIApplication.shared.windows[0].rootViewController!
vc.present(self.picker.viewController, animated: true) {
self.geoFolder = self.picker.geoFolder
}
#endif
}) {
Image(systemName: "book")
}
urlPickedView()
.hidden()
}
}
private func urlPickedView() -> some View {
DispatchQueue.main.async {
let geoF = self.picker.geoFolder
print("Committente: \(geoF.nomeCommittente) - Archivio: \(geoF.nomeArchivio)")
self.geoFolder = geoF
}
return TextField("", text: $geoFolder.geoFolderPath)
}
}
//File: ContentView
import SwiftUI
struct ContentView: View {
#State private var geoFolder = GeoFolderCodStruct()
var body: some View {
VStack {
HStack {
Text("Open GeoFolder File:")
.padding()
Spacer()
GeoFolderReadFileView(geoFolder: $geoFolder)
.padding()
}
.padding()
Group {
TextField("", text: $geoFolder.geoFolderPath)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("", text: $geoFolder.nomeCartella)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("", text: $geoFolder.nomeCommittente)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("", text: $geoFolder.nomeArchivio)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("", text: $geoFolder.note)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and the last, the json file to read for test the code.
{
"nomeCommittente" : "Appple",
"note" : "Image from Cupertino (CA).",
"latitudine" : 37.332161,
"longitudine" : -122.030352,
"nomeCartella" : "Foto",
"geoFolderPath" : "\/Users\/cesare\/Desktop",
"radiusCircle" : 50,
"dataCreazione" : "20\/03\/2020",
"nomeArchivio" : "AppleCampus-Image",
"isActive" : true
}
I was unable to implement the solution proposed by #Mdoyle1. I hope someone can edit the code to make it work as it should, without create hidden view.

Related

NSContactPicker not displaying picker window in SwiftUI on macOS

I have tried to get the NSContactPicker to display a picker window in SwiftUI on macOS. Here is my code. If you click on the button nothing happens. What am I missing?
import SwiftUI
import Contacts
import ContactsUI
let d = MyContactPicker()
class MyContactPicker: NSObject, CNContactPickerDelegate
{
var contactName: String = "No user selected"
func pickContact()
{
let contactPicker = CNContactPicker()
contactPicker.delegate = self
}
func contactPicker(_ picker: CNContactPicker, didSelect contact: CNContact)
{
contactName = contact.givenName
}
}
struct ContentView: View
{
#State var contact: CNContact?
var picker = MyContactPicker()
var body: some View
{
VStack
{
Text(picker.contactName)
Button("Select Contact")
{
picker.pickContact()
}
}
}
}
Here's a possible starting point using NSViewRepresentable and an NSView subclass
class NSContactPickerView: NSView, CNContactPickerDelegate {
let didSelectContact: (CNContact) -> Void
init(didSelectContact: #escaping (CNContact) -> Void) {
self.didSelectContact = didSelectContact
super.init(frame: .zero)
Task {
showPicker()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showPicker() {
let picker = CNContactPicker()
picker.delegate = self
picker.showRelative(to: .zero, of: self, preferredEdge: .maxY)
}
func contactPicker(_ picker: CNContactPicker, didSelect contact: CNContact) {
didSelectContact(contact)
}
}
struct ContactPicker: NSViewRepresentable {
let didSelectContact: (CNContact) -> Void
func makeNSView(context: Context) -> NSContactPickerView {
NSContactPickerView(didSelectContact: didSelectContact)
}
func updateNSView(_ nsView: NSContactPickerView, context: Context) {
}
}
struct ContentView: View {
#State var contact: CNContact?
#State private var showPicker = false
var body: some View {
VStack {
Text(contact?.givenName ?? "")
Button("Select Contact") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ContactPicker { contact in
self.contact = contact
}
.frame(width: 1, height: 1)
}
}
}
It works, but it's not very elegant. Maybe someone else can improve on this.

SwiftUI CoreData Preview FetchedResults Crashing

I am having issues previewing my SwiftUI CoreData in the canvas. The app works as expected in the simulator and on device, but the preview is crashing any preview view that uses a FetchedResult or ObservedObject (which I may be using wrong?).
My Persistence is set up as such, using a preview controller with supplied preview data:
import CoreData
import CloudKit
struct PersistenceController {
//MARK: - 1. PERSISTENCE CONTROLLER
static let shared = PersistenceController()
//MARK: - 2. PERSISTENT CONTAINER
let container: NSPersistentContainer
//MARK: - 3. INIT (load the persistent store)
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Training")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
//MARK: - 4. PREVIEW
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let newGoalView = GoalViews(context: viewContext)
newGoalView.startDate = K.init().EndOfWeek(weeksSinceAnchorDate: 1)
newGoalView.endDate = K.init().EndOfWeek(weeksSinceAnchorDate: 2)
newGoalView.id = UUID()
newGoalView.totalDistanceUnit = "meters"
newGoalView.totalDistanceDoubleValue = 30000
newGoalView.workoutActivityType = Int16(32)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo), The database failed to load.")
}
return result
}()
}
I am having issues both previewing it in the parent view here:
//
// GoalsView.swift
// Training
//
// Created by Liam Day on 03/05/2021.
//
import SwiftUI
import HealthKit
struct GoalsView: View {
//MARK: - PROPERTIES
#State var showNewGoalView: Bool = false
//MARK: - FETCHING DATA
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \GoalViews.endDate, ascending: true)],
animation: .default) var goalViews: FetchedResults<GoalViews>
func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { goalViews[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
//MARK: - BODY
var body: some View {
NavigationView {
List {
ForEach(goalViews) { goalView in
GoalViewListItemView(goalView: goalView)
}
.onDelete(perform: deleteItems)
}
.listStyle(PlainListStyle())
.navigationBarTitle(Text("Goals"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
self.showNewGoalView = true
}, label: {
Text("Add Goal")
})
}
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
.sheet(isPresented: $showNewGoalView, content: {
CreateGoalView(isShowing: $showNewGoalView)
})
}
}
}
struct GoalsView_Previews: PreviewProvider {
static var previews: some View {
return GoalsView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
And in the individual child view, GoalViewListItemView:
//
// GoalViewListItemView.swift
// Training
//
// Created by Liam Day on 30/05/2021.
//
import SwiftUI
import HealthKit
import CoreData
struct GoalViewListItemView: View {
var goalView: FetchedResults<GoalViews>.Element // changed from observed object
#State var progressValue: Double = 0
#AppStorage("distanceUnitSystemMetric") var distanceUnitSystemMetric: Bool = true
func calcGoalTotal() {
...
}
var body: some View {
NavigationLink(destination: GoalViewDetailView(goalView: goalView, progressValue: $progressValue)) {
VStack(alignment: .leading) {
Text(HKWorkoutActivityType.init(rawValue: UInt(goalView.workoutActivityType))!.commonName)
.font(.title)
Text(K.goalUnit.init(rawValue: (goalView.goalUnit ?? String("totalDistanceDoubleValue")))?.name ?? "Unkown")
Text("Start: \(Bundle.main.getDynamicDayName(date: goalView.startDate ?? Date()))")
Text("Finish: \(Bundle.main.getDynamicDayName(date: goalView.endDate ?? Date()))")
if goalView.goalUnit == K.goalUnit.totalDistanceDoubleValue.rawValue {
Text("Goal of \(Bundle.main.getDistance(totalDistanceDoubleValue: goalView.totalDistanceDoubleValue, totalDistanceUnit: HKUnit.meter().unitString, returnUnit: distanceUnitSystemMetric ? K.init().kilometer : HKUnit.mile().unitString, significantDigits: 3))")
ProgressView(
"\(Bundle.main.getDistance(totalDistanceDoubleValue: progressValue, totalDistanceUnit: HKUnit.meter().unitString, returnUnit: distanceUnitSystemMetric ? K.init().kilometer : HKUnit.mile().unitString, significantDigits: 3))",
value: (progressValue / goalView.totalDistanceDoubleValue),
total: 1
)
} else if goalView.goalUnit == K.goalUnit.totalEnergyBurnedDoubleValue.rawValue {
Text("Goal of \(String(format: "%.0f", goalView.totalEnergyBurnedDoubleValue)) KCal")
ProgressView(
"\(String(format: "%.0f", progressValue)) KCal",
value: (progressValue / goalView.totalEnergyBurnedDoubleValue),
total: 1
)
} else if goalView.goalUnit == K.goalUnit.duration.rawValue {
Text("Goal of \(Bundle.main.getHoursMinutesSeconds(timeInSeconds: goalView.duration))")
ProgressView(
"\(Bundle.main.getHoursMinutesSeconds(timeInSeconds: progressValue))",
value: (progressValue / goalView.duration),
total: 1
)
} else {
EmptyView()
}
}
}
.onAppear(perform: {
calcGoalTotal()
})
}
}
struct GoalViewListItemView_Previews: PreviewProvider {
static var previews: some View {
let viewContext = PersistenceController.preview.container.viewContext
let newGoalView = GoalViews(context: viewContext)
return GoalViewListItemView(goalView: newGoalView)
}
}
I have already swapped the child view from an #ObservedObject with type GoalViews to a FetchedResults<GoalViews>.Element.
The app continues to function as expected as I swap and change FetchedResults, ObservedObjects and States at different levels.
What is the correct data flow from parent (fetching data) to child (needing to access update/delete data)? How are these outlined in the preview if using a preview Persistence Controller?

How do I get a dropped pin inside a MapView to save coordinates and present them as a String using SwiftUI?

I made a Form using SwiftUI, in which I have a section that allows a user to press on a button that takes them to a MapKit View. Once inside the map view, the user can press the "+" button to place a pin on a map. This takes them to the Edit View where they can enter text inside a TextField to label the pin (see screenshot below). I have been stuck here for the past few days attempting to save the pin's coordinates or even the user's input inside the TextField to return it as text (either as city, state or country) inside the Form.
Form -> Map View -> Edit View
Here are some code snippets.
1) From FormView:
import SwiftUI
import MapKit
struct FormView: View {
#State private var selectedTitle = ""
#State var meuf: Meuf
#State private var meufs = [Meuf]()
#State private var show = false
#State private var singleIsPresented = false
#Environment(\.presentationMode) var presentationMode
let newMeuf : Bool
#EnvironmentObject var meufStorage : MeufStorage
#State private var showMap = false
var body: some View {
NavigationView{
Form{
//MARK: LOCATION
Section{
HStack {
Button(action: { self.showMap = true }) {
Image(systemName: "mappin.and.ellipse")
}
.sheet(isPresented: $showMap) {
LocationMap(showModal: self.$showMap)
}
Text("Pin your location")
.font(.subheadline)
}
}
// MARK: [ SAVE ENTRY ]
Section {
Button(action: {
if self.newMeuf {
self.saveData()
self.meufStorage.meufs.append(self.meuf)
} else {
for x in 0..<self.meufStorage.meufs.count {
if self.meufStorage.meufs[x].id == self.meuf.id {
self.meufStorage.meufs[x] = self.meuf
}
}
}
self.presentationMode.wrappedValue.dismiss()
}) {
HStack{
Spacer()
Text("Save")
Spacer()
}
}.disabled(meuf.title.isEmpty)
}
}.navigationBarTitle(Text(meuf.title))
}
}
//Get file directory url
func getFileDirectory() -> URL{
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
//Load data from file directory
func loadData(){
let filename = getFileDirectory().appendingPathComponent("SavedPlaces")
do {
let data = try Data(contentsOf: filename)
meufs = try JSONDecoder().decode([Meuf].self, from: data)
}catch{
debugPrint(error)
}
}
//Save data to file directory
func saveData(){
let filename = getFileDirectory().appendingPathComponent("SavedPlaces")
let data = try? JSONEncoder().encode(self.meufs)
do{
try data?.write(to: filename, options: [.atomic , .completeFileProtection])
}catch{
debugPrint(error)
}
}
}
struct FormView_Previews: PreviewProvider {
static var previews: some View {
FormView(meuf: Meuf(), newMeuf: true)
}
}
2) From LocationMap:
import SwiftUI
import MapKit
struct LocationMap: View {
#State private var centerCoordinate = CLLocationCoordinate2D()
#State private var locations = [CodableMKPointAnnotation]()
#State private var selectedPlace: MKPointAnnotation?
#State private var showingPlaceDetails = false
#State private var showingEditScreen = false
#Binding var showModal: Bool
var body: some View {
ZStack{
MapView(centerCoordinate: $centerCoordinate, annotations: locations, selectedPlace: $selectedPlace, showingPlaceDetails: $showingPlaceDetails)
.edgesIgnoringSafeArea(.all)
Circle()
.fill(Color.blue)
.opacity(0.3)
.frame(width: 32, height: 32)
VStack {
Spacer()
HStack{
Spacer()
Button(action:{
let newLocation = CodableMKPointAnnotation()
newLocation.title = ""
newLocation.coordinate = self.centerCoordinate
self.locations.append(newLocation)
self.selectedPlace = newLocation
self.showingEditScreen = true
}){
Image(systemName: "plus")
}
.padding()
.background(Color.black.opacity(0.7))
.foregroundColor(Color.white)
.clipShape(Circle())
.shadow(radius: 0.7)
.padding([.trailing , .bottom])
}
}
.padding()
}.alert(isPresented: $showingPlaceDetails) {
Alert(title: Text(selectedPlace?.title ?? "Unknown"), message: Text(selectedPlace?.subtitle ?? "Missing place information."), primaryButton: .default(Text("OK")), secondaryButton: .default(Text("Edit")) {
self.showingEditScreen = true
}
)
}
.sheet(isPresented: $showingEditScreen, onDismiss: savedData) {
if self.selectedPlace != nil {
EditView(placemark: self.selectedPlace!)
}
}
.onAppear(perform: loadData)
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func loadData() {
let filename = getDocumentsDirectory().appendingPathComponent("Saved Places")
do {
let data = try Data(contentsOf: filename)
locations = try JSONDecoder().decode([CodableMKPointAnnotation].self, from: data)
} catch {
print("Unable to load saved data.")
}
}
func savedData() {
do {
let filename = getDocumentsDirectory().appendingPathComponent("SavedPlaces")
let data = try JSONEncoder().encode(self.locations)
try data.write(to: filename, options: [.atomicWrite, .completeFileProtection])
} catch {
print("Unable to save data")
}
}
}
struct LocationMap_Previews: PreviewProvider {
static var previews: some View {
LocationMap(showModal: .constant(true))
}
}
3) From MapView:
import MapKit
import Combine
import SwiftUI
struct MapView: UIViewRepresentable {
#Binding var centerCoordinate: CLLocationCoordinate2D
var annotations: [MKPointAnnotation]
#Binding var selectedPlace: MKPointAnnotation?
#Binding var showingPlaceDetails: Bool
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
if annotations.count != uiView.annotations.count{
uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotations(annotations)
}
}
///Coordinator class for passing data
class Coordinator: NSObject , MKMapViewDelegate{
let parent: MapView
init(_ parent: MapView){
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
parent.centerCoordinate = mapView.centerCoordinate
}
//Gets called whenever the rightCalloutAccessory is tapped
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
guard let placeMark = view.annotation as? MKPointAnnotation else {return}
parent.selectedPlace = placeMark
parent.showingPlaceDetails = true
}
//Customizes the way the marker looks
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let identifier = "PlaceMark"
var annotationview = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationview == nil {
annotationview = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationview?.canShowCallout = true
annotationview?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
}else {
annotationview?.annotation = annotation
}
return annotationview
}
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
}
extension MKPointAnnotation {
static var example: MKPointAnnotation {
let annotation = MKPointAnnotation()
annotation.title = "Montreal"
annotation.subtitle = "Home of French Canadians"
annotation.coordinate = CLLocationCoordinate2D(latitude: 45.5, longitude: -73.58)
return annotation
}
}
struct MapView_Previews: PreviewProvider {
static var previews: some View {
MapView(centerCoordinate: .constant(MKPointAnnotation.example.coordinate), annotations: [MKPointAnnotation.example], selectedPlace: .constant(MKPointAnnotation.example), showingPlaceDetails: .constant(false))
}
}
4) From Edit View:
import SwiftUI
import MapKit
struct EditView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var placemark: MKPointAnnotation
var body: some View {
NavigationView {
Form {
Section {
TextField("Place name", text: $placemark.wrappedTitle)
TextField("Description", text: $placemark.wrappedSubtitle)
}
}
.navigationBarTitle("Edit place")
.navigationBarItems(trailing: Button("Done") {
self.presentationMode.wrappedValue.dismiss()
})
}
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(placemark: MKPointAnnotation.example)
}
}
5) MKPointAnnotation Codable
import Foundation
import MapKit
class CodableMKPointAnnotation: MKPointAnnotation , Codable {
enum codingKeys: CodingKey {
case title ,subtitle , longitude , latitude
}
override init() {
super.init()
}
public required init(from decoder: Decoder) throws{
super.init()
let container = try decoder.container(keyedBy: codingKeys.self)
title = try container.decode(String.self, forKey: .title)
subtitle = try container.decode(String.self, forKey: .subtitle)
let latitude = try container.decode(CLLocationDegrees.self, forKey: .latitude)
let longitude = try container.decode(CLLocationDegrees.self, forKey: .longitude)
coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: codingKeys.self)
try container.encode(title, forKey: .title)
try container.encode(subtitle, forKey: .subtitle)
try container.encode(coordinate.latitude, forKey: .latitude)
try container.encode(coordinate.longitude, forKey: .longitude)
}
}
6) MKPointAnnotation Object
import MapKit
extension MKPointAnnotation: ObservableObject{
public var wrappedTitle: String{
get{
self.title ?? "No Title"
}
set{
self.title = newValue
}
}
public var wrappedSubtitle: String{
get{
self.subtitle ?? "No information on this location"
}
set{
self.subtitle = newValue
}
}
}
7) Meuf & MeufStorage:
import Foundation
struct Meuf: Identifiable, Encodable, Decodable {
var id = UUID()
var img = ""
var title = ""
var rating = 3.0
var seen = false
var seenDate = ""
}
class MeufStorage: ObservableObject {
#Published var meufs = [Meuf]()
}
8) Scene Delegate:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let meufStorage = MeufStorage()
let contentView = MeufList().environment(\.managedObjectContext, context).environmentObject(meufStorage)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
}
Hello I managed to get the solution.
Will add code only which is changed
// Made this a ObservableObject
class Meuf: Identifiable, Codable, ObservableObject {
var id = UUID()
var img = ""
var title = ""
var rating = 3.0
var seen = false
var seenDate = ""
var locations = [CodableMKPointAnnotation]() // We need this to keep the track of locations
}
FormView
struct FormView: View {
#State private var selectedTitle = ""
#ObservedObject var meufObject = Meuf() // This is new will help to keep track of the added locations
#State private var meufs = [Meuf]()
#State private var show = false
#State private var singleIsPresented = false
#Environment(\.presentationMode) var presentationMode
#EnvironmentObject var meufStorage : MeufStorage
#State private var showMap = false
var body: some View {
NavigationView{
Form {
List {
// This will list the added locations now
ForEach(self.meufObject.locations, id: \.self) { location in
LocationView(location: location)
}
}
//MARK: LOCATION
Section{
HStack {
Button(action: { self.showMap = true }) {
Image(systemName: "mappin.and.ellipse")
}
.sheet(isPresented: $showMap) {
LocationMap(meufObject: self.meufObject, showModal: self.$showMap)
}
Text("Pin your location")
.font(.subheadline)
}
}
// MARK: [ SAVE ENTRY ]
Section {
Button(action: {
// Handle save action
}) {
HStack{
Spacer()
Text("Save")
Spacer()
}
}
}
}
}
}
// Rest of your code stays same .......
}
// Added this new view to render the location view
struct LocationView: View {
var location : CodableMKPointAnnotation
var body: some View {
Text(location.title ?? "title" )
}
}
LocationMap
struct LocationMap: View {
#ObservedObject var meufObject: Meuf // This is new will help to keep track of the added locations
#State private var centerCoordinate = CLLocationCoordinate2D()
#State private var locations = [CodableMKPointAnnotation]()
#State private var selectedPlace: MKPointAnnotation?
#State private var showingPlaceDetails = false
#State private var showingEditScreen = false
#Environment(\.presentationMode) var presentationMode
#Binding var showModal: Bool
var body: some View {
ZStack{
MapView(centerCoordinate: $centerCoordinate, annotations: locations, selectedPlace: $selectedPlace, showingPlaceDetails: $showingPlaceDetails)
.edgesIgnoringSafeArea(.all)
Circle()
.fill(Color.blue)
.opacity(0.3)
.frame(width: 32, height: 32)
VStack {
Spacer()
HStack{
Spacer()
Button(action:{
let newLocation = CodableMKPointAnnotation()
newLocation.title = ""
newLocation.coordinate = self.centerCoordinate
self.locations.append(newLocation)
self.meufObject.locations = self.locations // By doing this we will be able to pass it to main screen
self.selectedPlace = newLocation
self.showingEditScreen = true
}){
Image(systemName: "plus")
}
.padding()
.background(Color.black.opacity(0.7))
.foregroundColor(Color.white)
.clipShape(Circle())
.shadow(radius: 0.7)
.padding([.trailing , .bottom])
// Rest stays same as your implementation
}
}
.padding()
}
}
// Rest stays same as your implementation
}

Why does my TabView change back to Home tab when image loads?

I created a TabView with two tabs. One is Home and the other loads text and an image from NASA pic of the day API. When I change to the NASA pic of the day, I see "Loading data" until the data loads. Once the data is loaded, for some reason the tab switches back to the "Home" tab. After this bug happens, I can switch back and forth between the two tabs normally and everything is loaded. Why does the tab get switched back to the home tab? Thank you!!
APIImageView Code:
import SwiftUI
struct ApiImageView: View {
#ObservedObject var apiImage = ApiImage()
var body: some View {
Group {
if apiImage.dataHasLoaded {
VStack {
Text(apiImage.title!)
.font(.largeTitle)
Image(uiImage: apiImage.image!).resizable()
.cornerRadius(10)
.padding()
ScrollView(.vertical, showsIndicators: false) {
Text(apiImage.explanation!)
.font(.subheadline)
.padding()
}
}
} else {
Text("Loading Data")
}
}.onAppear {
self.apiImage.loadImageFromApi(urlString: "https://api.nasa.gov/planetary/apod?api_key=eaRYg7fgTemadUv1bQawGRqCWBgktMjolYwiRrHK")
}
}
}
struct ApiImageView_Previews: PreviewProvider {
static var previews: some View {
ApiImageView()
}
}
APIImage Code:
import SwiftUI
class ApiImage: ObservableObject {
#Published var dataHasLoaded = false
#Published var image: UIImage? = nil
#Published var title: String? = nil
#Published var explanation: String? = nil
}
extension ApiImage {
func loadImageFromApi(urlString: String) {
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request, completionHandler: parseJsonObject)
task.resume()
}
func parseJsonObject(data: Data?, urlResponse: URLResponse?, error: Error?) {
guard error == nil else {
print("\(error!)")
return
}
guard let content = data else {
print("No data")
return
}
let json = try! JSONSerialization.jsonObject(with: content)
let jsonmap = json as! [String : Any]
let titleText = jsonmap["title"] as! String
let explanationText = jsonmap["explanation"] as! String
let urlString = jsonmap["url"] as! String
print("\(urlString)")
print("\(titleText)")
print("\(explanationText)")
DispatchQueue.main.async {
self.title = titleText
self.explanation = explanationText
}
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request, completionHandler: setImageFromData)
task.resume()
}
func setImageFromData(data: Data?, urlResponse: URLResponse?, error: Error?) {
guard error == nil else {
print("\(error!)")
return
}
guard let content = data else {
print("No data")
return
}
DispatchQueue.main.async {
self.image = UIImage(data: content)
self.dataHasLoaded = true
}
}
}
MainTabView Code:
import SwiftUI
struct MainTabView: View {
var body: some View {
TabView {
CategoryHome()
.tabItem {
Image(systemName: "house.fill")
Text("Landmarks")
.tag(0)
}
ApiImageView()
.tabItem {
Image(systemName: "flame.fill")
Text("NASA Pic")
//.tag(1)
}
}
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView()
}
}
Maybe not directly a solution, but also important: it seems that the MainTabView is not entirely correct (the .tag() should be outside the .tabItem closure). This would be a correct version:
import SwiftUI
struct MainTabView: View {
var body: some View {
TabView {
CategoryHome()
.tabItem {
Image(systemName: "house.fill")
Text("Landmarks")
}.tag(0)
ApiImageView()
.tabItem {
Image(systemName: "flame.fill")
Text("NASA Pic")
}.tag(1)
}
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView()
}
}
Maybe this is already the solution; if not, I hope it is still helpful! :)

UIView[Controller]Representable: SwiftUI Subview is shown in Debugger, but not when run

Because the ScrollView does not provide a function to set the contentOffset, I'm trying to use the UIScrollView as UIViewRepresentable. The attached code shows both, the caller and the definition of the view and the view controller.
When running the code in simulator or previews, just a blue area is shown. When debugging the display, the Text is shown, as expected.
If have no idea about the reason - is it because I'm doing something wrong, or because there's a bug in Xcode or SwiftUI?
Here the custom scroll view:
struct PositionableScrollView<Content>: UIViewRepresentable where Content: View {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: UIViewRepresentableContext<PositionableScrollView<Content>>) -> UIScrollView {
let scrollViewVC = PositionableScrollViewVC<Content>(nibName: nil, bundle: nil)
scrollViewVC.add(content: content)
let control = scrollViewVC.scrollView
return control
}
func updateUIView(_ uiView: UIScrollView, context: UIViewRepresentableContext<PositionableScrollView<Content>>) {
// Do nothing at the moment.
}
}
The view controller:
final class PositionableScrollViewVC<Content>: UIViewController where Content: View {
var scrollView: UIScrollView = UIScrollView()
var contentView: UIView!
var contentVC: UIViewController!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
self.view.addSubview(self.scrollView)
self.scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
self.scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
self.scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
self.scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLoad() {
super.viewDidLoad()
debugPrint("self:", self.frame())
debugPrint("self.view:", self.view!.frame)
debugPrint("self.view.subviews:", self.view.subviews)
// debugPrint("self.view.subviews[0]:", self.view.subviews[0])
// debugPrint("self.view.subviews[0].subviews:", self.view.subviews[0].subviews)
}
func add(#ViewBuilder content: #escaping () -> Content) {
self.contentVC = UIHostingController(rootView: content())
self.contentView = self.contentVC.view!
self.scrollView.addSubview(contentView)
self.contentView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor).isActive = true
self.contentView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor).isActive = true
self.contentView.topAnchor.constraint(equalTo: self.scrollView.topAnchor).isActive = true
self.contentView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor).isActive = true
self.contentView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.widthAnchor.constraint(greaterThanOrEqualTo: self.scrollView.widthAnchor).isActive = true
}
}
extension PositionableScrollViewVC: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<PositionableScrollViewVC>) -> PositionableScrollViewVC {
let vc = PositionableScrollViewVC()
return vc
}
func updateUIViewController(_ uiViewController: PositionableScrollViewVC, context: UIViewControllerRepresentableContext<PositionableScrollViewVC>) {
// Do nothing at the moment.
}
}
The callers:
struct TimelineView: View {
#State private var posX: CGFloat = 0
var body: some View {
GeometryReader { geo in
VStack {
Text("\(self.posX) || \(geo.frame(in: .global).width)")
PositionableScrollView() {
VStack {
Spacer()
Text("Hallo")
.background(Color.yellow)
Spacer()
}
.frame(width: 1000, height: 200, alignment: .bottomLeading)
}
.background(Color.blue)
}
}
}
}
struct TimelineView_Previews: PreviewProvider {
static var previews: some View {
TimelineView()
}
}
The display, when run, and in debugger:

Resources