That is my projects link https://github.com/m3rtkoksal/TaskManager
This is my TaskListView
This is my NewTaskView
Unfortunately when I tap one of the items in scrollview again I see an empty NewTaskView. I should see NewTaskView with selectedTask datas instead.
That is my NewTaskView
struct NewTaskView: View {
#Environment(\.presentationMode) private var presentationMode
#StateObject private var obser = observer()
#State var taskTitle = ""
#State var taskFrom = ""
#State var taskFromDate = Date()
#State var taskToDate = Date()
#State var taskTo = ""
#State var taskNote = ""
#EnvironmentObject var task: SelectedTask
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Group {
Text("Task Title")
TextField("Title", text:$taskTitle)
Divider()
Text("From")
DatePicker("", selection: $taskFromDate, in: Date()..., displayedComponents: [.date, .hourAndMinute])
.labelsHidden()
.onReceive(Just(taskFromDate)) { data in
taskFrom = getDate(date: taskFromDate)
}
Divider()
} .padding(.horizontal, 10)
.font(Font.custom("SFCompactDisplay-Bold", size: 25))
.foregroundColor(.gray)
Group {
Text("To")
DatePicker("", selection: $taskToDate, in: Date()..., displayedComponents: [.date, .hourAndMinute])
.labelsHidden()
.onReceive(Just(taskToDate)) { data in
taskTo = getDate(date: taskToDate)
}
Divider()
Text("Note")
TextField("Note", text:$taskNote)
}.padding(.horizontal, 10)
.font(Font.custom("SFCompactDisplay-Bold", size: 25))
.foregroundColor(.gray)
Button(action: {
let taskDictionary = [
"title" : self.taskTitle,
"dateTo": self.taskTo,
"dateFrom" : self.taskFrom,
"text": self.taskNote
]
let docRef = Firestore.firestore().document("tasks/\(UUID().uuidString)")
docRef.setData(taskDictionary) { (error) in
if let error = error {
print("error = \(error)")
} else {
print("success")
self.taskTitle = ""
self.taskNote = ""
}
}
}, label: {
ZStack {
RoundedRectangle(cornerRadius: 25)
.foregroundColor(Color(#colorLiteral(red: 0.4274509804, green: 0.2196078431, blue: 1, alpha: 1)))
Text("Create task")
.foregroundColor(.white)
.font(.title)
.fontWeight(.bold)
}
.padding(.horizontal, 10)
.frame(width: UIScreen.main.bounds.width - 20, height: 90)
})
}
}
.navigationBarTitle("Create a task")
.font(Font.custom("SFCompactDisplay-Bold", size: 30))
}
}
And this is how I append selectedTask and call NewTaskView()
import SwiftUI
struct TaskFrameView: View {
#ObservedObject private var obser = observer()
var body: some View {
VStack(alignment: .leading){
Text("Today task")
.padding()
ScrollViewTask()
}
}
}
struct ScrollViewTask: View {
#EnvironmentObject var selectedTask : SelectedTask
#State var shown: Bool = false
#ObservedObject private var obser = observer()
var body: some View {
ScrollView(.vertical) {
VStack {
ForEach(self.obser.tasks) { task in
TaskElementView(task:task)
.onTapGesture {
self.shown.toggle()
self.selectedTask.appendNewTask(task: task)
}
}
}
}
.onAppear {
self.obser.fetchData()
}
.fullScreenCover(isPresented: $shown, content: {
NewTaskView()
.environmentObject(selectedTask)
})
}
}
How should I modify NewTaskView to be able to see selectedTask datas when an item is selected on Scrollview?
I have tried like below but I am getting index out of range error when I try to add new item. Plus there must be a better way than doing it with if else
VStack(alignment: .leading) {
Group {
Text("Task Title")
if task.item[0].title == "" {
TextField("Title", text:$taskTitle)
} else {
TextField(task.item[0].title, text: $taskTitle)
}
You have to add these code in SceneDelegate.swift
var selectedTask = SelectedTask()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView()
.environmentObject(selectedTask)
)
self.window = window
window.makeKeyAndVisible()
}
}
Related
I'm writing a macOS app in Swiftui, for Big Sur and newer. It's a three pane navigationview app, where the left most pane has the list of options (All Notes in this case), the middle pane is a list of the actual items (title and date), and the last one is a TextEditor where the user adds text.
Each pane is a view that calls the the next view via a NavigationLink. Here's the basic code for that.
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
struct ContentView: View {
#State var selection: Set<Int> = [0]
var body: some View {
NavigationView {
List(selection: self.$selection) {
NavigationLink(destination: AllNotes()) {
Label("All Notes", systemImage: "doc.plaintext")
}
.tag(0)
}
.listStyle(SidebarListStyle())
.frame(minWidth: 100, idealWidth: 150, maxWidth: 200, maxHeight: .infinity)
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct AllNotes: View {
#State var items: [NoteItem] = {
guard let data = UserDefaults.standard.data(forKey: "notes") else { return [] }
if let json = try? JSONDecoder().decode([NoteItem].self, from: data) {
return json
}
return []
}()
#State var noteText: String = ""
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(destination: NoteView()) {
VStack(alignment: .leading) {
Text(item.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(item.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
NewNote()
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
struct NoteView: View {
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
print("Value of text modified to = \(text)")
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
When I create a new note, how can I save the text the user added on the TextEditor in NoteView in the array loaded in AllNotes so I could save the new text? Ideally there is a SaveNote() function that would happen on TextEditor .onChange. But again, given that the array lives in AllNotes, how can I update it from other views?
Thanks for the help. Newbie here!
use EnvironmentObject in App
import SwiftUI
#main
struct NotesApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DataModel())
}
}
}
now DataModel is a class conforming to ObservableObject
import SwiftUI
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
any data related stuff should be done in DataModel not in View, plus you can access it and update it from anywhere, declare it like this in your ContentView or any child View
NoteView
import SwiftUI
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
.onAppear() {
print(data.notes.count)
}
}
}
AppStorage is the better way to use UserDefaults but AppStorage does not work with custom Objects yet (I think it does for iOS 15), so you need to add this extension to make it work.
import SwiftUI
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
let df = DateFormatter()
df.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return df.string(from: date)
}
var tags: [String] = []
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
Now I changed AllNotes view to work with new changes
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(destination: NoteView(note: note)) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
}
SwiftUI offers .animation() on bindings that will animate changes in the view. But if an #Published property from an #ObserveredObject changes 'autonomously' (e.g., from a timer), while the view will update in response to the change, there is no obvious way to get the view to animate the change.
In the example below, when isOn is changed from the Toggle, it animates, but when changed from the Timer it does not. Interestingly, if I use a ternary conditional here rather than if/else even the toggle will not trigger animation.
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
if model.isOn {
MyImage(color: .blue)
} else {
MyImage(color: .clear)
}
Spacer()
Toggle("switch", isOn: $model.isOn.animation(.easeIn(duration: 0.5)))
Spacer()
}
}
}
struct MyImage: View {
var color: Color
var body: some View {
Image(systemName: "pencil.circle.fill")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(color)
}
}
class Model: ObservableObject {
#Published var isOn: Bool = false
var timer = Timer()
init() {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [unowned self] _ in
isOn.toggle()
})
}
}
How can I trigger animations when the value changes are not coming from a binding?
The easiest option is to add a withAnimation block inside your timer closure:
withAnimation(.easeIn(duration: 0.5)) {
isOn.toggle()
}
If you don't have the ability to change the #ObservableObject closure, you could add a local variable to mirror the changes:
struct ContentView: View {
#ObservedObject var model: Model
#State var localIsOn = false
var body: some View {
VStack {
if localIsOn {
MyImage(color: .blue)
} else {
MyImage(color: .clear)
}
Spacer()
Toggle("switch", isOn: $model.isOn.animation(.easeIn(duration: 0.5)))
Spacer()
}.onChange(of: model.isOn) { (on) in
withAnimation {
localIsOn = on
}
}
}
}
You could also do a similar trick with a mirrored variable inside your ObservableObject:
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
if model.animatedOn {
MyImage(color: .blue)
} else {
MyImage(color: .clear)
}
Spacer()
Toggle("switch", isOn: $model.isOn.animation(.easeIn(duration: 0.5)))
Spacer()
}
}
}
class Model: ObservableObject {
#Published var isOn: Bool = false
#Published var animatedOn : Bool = false
var cancellable : AnyCancellable?
var timer = Timer()
init() {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [unowned self] _ in
isOn.toggle()
})
cancellable = $isOn.sink(receiveValue: { (on) in
withAnimation {
self.animatedOn = on
}
})
}
}
You can use an implicit animation for that, i.e. .animation(_:value:), e.g.
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Group {
if model.isOn {
MyImage(color: .blue)
} else {
MyImage(color: .clear)
}
}
.animation(Animation.default, value: model.isOn)
}
}
}
withAnimation is called explicit.
My full project is here https://github.com/m3rtkoksal/TaskManager
I have made SelectedTask an environment object as below
let context = persistentContainer.viewContext
let contentView = ContentView()
.environmentObject(observer())
.environmentObject(SelectedTask())
.environment(\.managedObjectContext,context)
In my TaskElement model I have created another class called SelectedTask as below
class SelectedTask: ObservableObject {
#Published var item = [TaskElement]()
func appendNewTask(task: TaskElement) {
objectWillChange.send()
item.append(TaskElement(title: task.title, dateFrom: task.dateFrom , dateTo: task.dateTo , text: task.text))
}
}
I am trying to fetch an item inside the scroll view and get its data to be able to modify it in the NewTaskView as below
struct ScrollViewTask: View {
#ObservedObject private var obser = observer()
#EnvironmentObject var selectedTask : SelectedTask
#State var shown: Bool = false
var body: some View {
ScrollView(.vertical) {
VStack {
ForEach(self.obser.tasks) { task in
TaskElementView(task:task)
.onTapGesture {
self.selectedTask.objectWillChange.send()
self.selectedTask.appendNewTask(task: task) //THREAD 1 ERROR
print(task)
self.shown.toggle()
}
}
}
}
.onAppear {
self.obser.fetchData()
}
.fullScreenCover(isPresented: $shown, content: {
NewTaskView(isShown: $shown)
.environmentObject(selectedTask)
})
}
}
But when I tap one of the items in scrollview I am getting a Thread 1 error #self.selectedTask.appendNewTask(task: task)
Thread 1: Fatal error: No ObservableObject of type SelectedTask found. A View.environmentObject(_:) for SelectedTask may be missing as an ancestor of this view.
If I change as ScrollViewTask().environmentObject(self.obser)
then this happens
This is how my TaskFrameView is called
import SwiftUI
struct TaskListView: View {
#State private(set) var data = ""
#State var isSettings: Bool = false
#State var isSaved: Bool = false
#State var shown: Bool = false
#State var selectedTask = TaskElement(title: "", dateFrom: "", dateTo: "", text: "")
var body: some View {
NavigationView {
ZStack {
Color(#colorLiteral(red: 0.9333333333, green: 0.9450980392, blue: 0.9882352941, alpha: 1)).edgesIgnoringSafeArea(.all)
VStack {
TopBar()
HStack {...}
CustomSegmentedView()
ZStack {
TaskFrameView() // scrollview inside
VStack {
Spacer()
HStack {...}
}
NavigationLink(
destination: NewTaskView(isShown: $shown).environmentObject(selectedTask),
isActive: $shown,
label: {
Text("")
})
}
}
}
.navigationBarHidden(true)
Spacer()
}
.navigationBarHidden(true)
}
}
It looks like the selectedTask is not injected to the TaskListView.
Find the place where you call TaskListView() and inject the selectedTask as an EnvironmentObject.
In ContentView:
struct ContentView: View {
#EnvironmentObject var selectedTask : SelectedTask
...
TaskListView().environmentObject(selectedTask)
Also don't create new instances of selectedTask like:
#State var selectedTask = TaskElement(title: "", dateFrom: "", dateTo: "", text: "")
Get the already created instance from the environment instead:
#EnvironmentObject var selectedTask: SelectedTask
call scroll view by sending the observer object as environment object modifier
ScrollViewTask().environmentObject(self. observer)
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
}
In the following code, I cannot seem to get the FORM view to update when I change the value (and therefore state) of some of the fields, when using a CustomTextField. I imagine that the problem is in the coordination between the CustomTextField and SwiftUI, but I get the values when I do the calculations (below), but I can't get the updated values to display in the relevant UITextFields onscreen.
Can anyone spot my error? Any ideas would be greatly appreciated.
Thanks a lot.
Chris
struct CustomTextField: UIViewRepresentable {
var tag:Int = 0
var placeholder:String?
var keyboardType:UIKeyboardType?
var textAlignment:NSTextAlignment?
#Binding var text: String
var onChange: (()->Void?)?
func makeCoordinator() -> Coordinator {
Coordinator(text: $text, onChange: onChange)
}
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let tmpView = UITextField()
tmpView.tag = tag
tmpView.delegate = context.coordinator as UITextFieldDelegate
tmpView.placeholder = placeholder
tmpView.textAlignment = textAlignment ?? .left
tmpView.keyboardType = keyboardType ?? .default
tmpView.addDoneButtonOnKeyboard()
return tmpView
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
class Coordinator : NSObject, UITextFieldDelegate {
#Binding var text: String
var onChange:(()->Void?)?
init(text: Binding<String>, onChange: (()->Void?)?) {
self._text = text
self.onChange = onChange
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let currentTag = textField.tag as Int? {
if currentTag == 1 {
if string.count > 0 /*&& !textField.text!.contains(".")*/ {
let tmpCents = textField.text?.replacingOccurrences(of: ".", with: "") ?? ""
let cents = Int( tmpCents + string) ?? 0
if cents == 0 {
textField.text = "0.00"
} else {
let dols = Float(cents)/Float(100)
textField.text = String(format: "%0.2f", dols)
}
self.text = textField.text!
return false
}
}
}
if let currentValue = textField.text as NSString? {
let proposedValue = currentValue.replacingCharacters(in: range, with: string)
text = proposedValue
}
return true
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
self.text = textField.text ?? ""
return true
}
func textFieldDidChange(_ textField: UITextField) {
self.text = textField.text ?? ""
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.onChange?()
textField.resignFirstResponder()
}
}
}
struct DetailView: View {
#EnvironmentObject var log: GasLog
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var amount = ""
#State var pickedDate = Date()
#State var kilometers = ""
#State var editingAmount = false
#State var litres = ""
#State var gasPrice = ""
#State var showAlert = false
#State var errorMessage = ""
#State var reloadContent = false
var dateClosedRange: ClosedRange<Date> {
let min = Calendar.current.date(byAdding: .day, value: -10, to: Date())!
let max = Calendar.current.date(byAdding: .day, value: 10, to: Date())!
return min...max
}
var body: some View {
VStack {
HStack {
Text("ADD GAS PURCHASE")
.font(defaultSectionFont)
}
Form {
DatePicker(selection: $pickedDate, displayedComponents: .date, label: {
Text("PURCHASE DATE").font(defaultFormFont)
} )
HStack {
Text("AMOUNT").font(defaultFormFont)
Spacer()
CustomTextField(tag: 1,
placeholder: "purchase amount",
keyboardType: .numberPad,
textAlignment: .right,
text: $amount,
onChange: nil)
}
HStack {
Text("LITRES").font(defaultFormFont)
Spacer()
CustomTextField(tag: 1, placeholder: "litres purchased", keyboardType: .numberPad,
textAlignment: .right, text: $litres, onChange: self.calcValues)
}
HStack {
Text("FUEL PRICE").font(defaultFormFont)
Spacer()
CustomTextField(tag: 1, placeholder: "fuel price", keyboardType: .numberPad,
textAlignment: .right, text: $gasPrice, onChange: self.calcValues)
}
HStack {
Text("KILOMETERS ON CAR").font(defaultFormFont)
Spacer()
CustomTextField(tag: 0, placeholder: "kilometers", keyboardType: .numberPad,
textAlignment: .right, text: $kilometers, onChange: nil)
}
}
HStack {
Spacer()
Button(action: {
self.cancelRecord()
}, label: {
Image(systemName: "return")
})
.padding()
.overlay(
RoundedRectangle(cornerRadius: CGFloat(8.0))
.stroke(Color.gray, lineWidth: CGFloat(2.0))
)
Spacer()
Button(action: {
self.commitRecord()
}, label: {
Image(systemName: "plus.square")
})
.padding()
.overlay(
RoundedRectangle(cornerRadius: CGFloat(8.0))
.stroke(Color.gray, lineWidth: CGFloat(2.0))
)
Spacer()
}
.padding()
.background(toolbarBackgroundColor)
}.alert(isPresented: $showAlert) {
Alert(title: Text("Error"), message: Text(self.errorMessage))
}
}
func calcValues() -> Void {
if !self.amount.isEmpty {
switch (!self.gasPrice.isEmpty, !self.litres.isEmpty) {
case (true, false) :
self.litres = String(format: "%0.2f", Float(self.amount)! / Float(self.gasPrice)!)
self.reloadContent = true
case (false, true) :
self.gasPrice = String(format: "%0.2f", Float(self.amount)! / Float(self.litres)!)
self.reloadContent = true
default :
self.reloadContent = false
}
}
}
func commitRecord() {
let log = GasLog.shared()
if self.amount.isEmpty || Float(self.amount) == 0.0 {
errorMessage = "Value of AMOUNT is invalid. Please re-enter."
showAlert = true
} else {
self.dismiss()
log.addLogItem(date: self.pickedDate,
amount: (self.amount.isEmpty ? 0.00 : Float(self.amount)!),
kilometers: (self.kilometers.isEmpty ? nil : Int(self.kilometers)),
gasPrice: (self.gasPrice.isEmpty ? nil : Float(self.gasPrice)),
litres: (self.litres.isEmpty ? nil : Float(self.litres)))
}
}
func cancelRecord() {
self.dismiss()
}
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
}
Add uiView.text = text to updateUIView:
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
uiView.text = text // add this
}