View update problem with UITextField integration into SwiftUI - uitextfield

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
}

Related

How to add a limit to the lines of text in a customized text field?

I have a customized text field that allows me to write multiple lines of text. However, I am wondering if there is a way to limit how many lines this text field will allow you to write. Setting a frame doesn't seem to work, as once you pass the frame's limit, you just continue to scroll down new lines. Something like TikTok or Instagram's edit bio forms, which after a certain amount of returns, you can't go any further.
Here is what I have so far:
struct NewSnippetTextEditor : UIViewRepresentable {
#Binding var txt : String
var doppleGray = Color(hue: 1.0, saturation: 0.058, brightness: 0.396)
#StateObject var profileData = ProfileViewModel()
func makeCoordinator() -> NewSnippetTextEditor.NewSnippetCoordinator {
return NewSnippetTextEditor.NewSnippetCoordinator(parent1: self)
}
func makeUIView(context: UIViewRepresentableContext<NewSnippetTextEditor>) -> UITextView {
let tview = UITextView()
tview.isEditable = true
tview.isUserInteractionEnabled = true
tview.isScrollEnabled = true
tview.text = "What's up..."
tview.textColor = .gray
tview.font = .systemFont(ofSize: 18)
tview.keyboardType = .twitter
tview.delegate = context.coordinator
return tview
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<NewSnippetTextEditor>) {
}
class NewSnippetCoordinator : NSObject, UITextViewDelegate{
var parent : NewSnippetTextEditor
init(parent1 : NewSnippetTextEditor) {
parent = parent1
}
func textViewDidChange(_ textView: UITextView) {
self.parent.txt = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
textView.text = ""
textView.textColor = .label
}
}
}
And then to display this in a view I use:
NewSnippetTextEditor(txt: $newSnippetData.snippetTxt)
.cornerRadius(20)
.padding(EdgeInsets.init(top: 0, leading: 20, bottom: 0, trailing: 40))
.opacity(newSnippetData.isPosting ? 0.5 : 1)
.disabled(newSnippetData.isPosting ? true : false)
If this doesn't work for you, I also have another version that you might be able to better tweak.
struct NewSnippetTextView: UIViewRepresentable{
#Binding var text:String
#Binding var height:CGFloat
var placeholderText: String
#State var editing:Bool = false
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.isEditable = true
textView.isScrollEnabled = true
textView.text = placeholderText
textView.delegate = context.coordinator
textView.textColor = UIColor.white
textView.backgroundColor = UIColor.black
textView.font = UIFont.systemFont(ofSize: 18)
textView.keyboardType = .twitter
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
if self.text.isEmpty == true{
textView.text = self.editing ? "" : self.placeholderText
textView.textColor = self.editing ? .white : .lightGray
}
DispatchQueue.main.async {
self.height = textView.contentSize.height
textView.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
}
}
func makeCoordinator() -> Coordinator {
NewSnippetTextView.Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate{
var parent: NewSnippetTextView
init(_ params: NewSnippetTextView) {
self.parent = params
}
func textViewDidBeginEditing(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.editing = true
}
}
func textViewDidEndEditing(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.editing = false
}
}
func textViewDidChange(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.height = textView.contentSize.height
self.parent.text = textView.text
}
}
}
}
To display this second version I used:
NewSnippetTextView(text: $newSnippetData.snippetTxt, height: $textViewHeight, placeholderText: "What's up")
.frame(height: textViewHeight < 160 ? self.textViewHeight : 160)
.cornerRadius(20)
.padding(EdgeInsets.init(top: 0, leading: 20, bottom: 0, trailing: 40))
.opacity(newSnippetData.isPosting ? 0.5 : 1)
.disabled(newSnippetData.isPosting ? true : false)
How about using a standard TextEditor and checking its length at change:
(I admit it only counts characters, not lines, but you could implement that too by counting line breaks)
TextEditor(text: $textInput)
.onChange(of: textInput) { newValue in
if newValue.count > 30 {
textInput = String(newValue.dropLast(1))
}
}

Capture video : SwiftUI

I want to capture a video through back camera using swiftUI. I can not find the proper solution on on video recording. I implement the code that record video automatically when view is open But I want to start the recording on bottom button click. Can someone please guide me on this.
import SwiftUI
import AVKit
struct RecordingView: View {
#State private var timer = 5
#State private var onComplete = false
#State private var recording = false
var body: some View {
ZStack {
VideoRecordingView(timeLeft: $timer, onComplete: $onComplete, recording: $recording)
VStack {
Button(action: {self.recording.toggle()}, label: {
ZStack {
Circle()
.fill(Color.white)
.frame(width: 65, height: 65)
Circle()
.stroke(Color.white,lineWidth: 2)
.frame(width: 75, height: 75)
}
})
Button(action: {
self.timer -= 1
print(self.timer)
}, label: {
Text("Toggle timer")
})
.foregroundColor(.white)
.padding()
Button(action: {
self.onComplete.toggle()
}, label: {
Text("Toggle completion")
})
.foregroundColor(.white)
.padding()
}
}
}
}
This is For recordingView
struct VideoRecordingView: UIViewRepresentable {
#Binding var timeLeft: Int
#Binding var onComplete: Bool
#Binding var recording: Bool
func makeUIView(context: UIViewRepresentableContext<VideoRecordingView>) -> PreviewView {
let recordingView = PreviewView()
recordingView.onComplete = {
self.onComplete = true
}
recordingView.onRecord = { timeLeft, totalShakes in
self.timeLeft = timeLeft
self.recording = true
}
recordingView.onReset = {
self.recording = false
self.timeLeft = 30
}
return recordingView
}
func updateUIView(_ uiViewController: PreviewView, context: UIViewRepresentableContext<VideoRecordingView>) {
}
}
extension PreviewView: AVCaptureFileOutputRecordingDelegate{
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
print(outputFileURL.absoluteString)
}
}
class PreviewView: UIView {
private var captureSession: AVCaptureSession?
private var shakeCountDown: Timer?
let videoFileOutput = AVCaptureMovieFileOutput()
var recordingDelegate:AVCaptureFileOutputRecordingDelegate!
var recorded = 0
var secondsToReachGoal = 30
var onRecord: ((Int, Int)->())?
var onReset: (() -> ())?
var onComplete: (() -> ())?
init() {
super.init(frame: .zero)
var allowedAccess = false
let blocker = DispatchGroup()
blocker.enter()
AVCaptureDevice.requestAccess(for: .video) { flag in
allowedAccess = flag
blocker.leave()
}
blocker.wait()
if !allowedAccess {
print("!!! NO ACCESS TO CAMERA")
return
}
// setup session
let session = AVCaptureSession()
session.beginConfiguration()
let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video, position: .front)
guard videoDevice != nil, let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!), session.canAddInput(videoDeviceInput) else {
print("!!! NO CAMERA DETECTED")
return
}
session.addInput(videoDeviceInput)
session.commitConfiguration()
self.captureSession = session
}
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
recordingDelegate = self
startTimers()
if nil != self.superview {
self.videoPreviewLayer.session = self.captureSession
self.videoPreviewLayer.videoGravity = .resizeAspect
self.captureSession?.startRunning()
self.startRecording()
} else {
self.captureSession?.stopRunning()
}
}
private func onTimerFires(){
print("🟢 RECORDING \(videoFileOutput.isRecording)")
secondsToReachGoal -= 1
recorded += 1
onRecord?(secondsToReachGoal, recorded)
if(secondsToReachGoal == 0){
stopRecording()
shakeCountDown?.invalidate()
shakeCountDown = nil
onComplete?()
videoFileOutput.stopRecording()
}
}
func startTimers(){
if shakeCountDown == nil {
shakeCountDown = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] (timer) in
self?.onTimerFires()
}
}
}
func startRecording(){
captureSession?.addOutput(videoFileOutput)
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let filePath = documentsURL.appendingPathComponent("tempPZDC")
videoFileOutput.startRecording(to: filePath, recordingDelegate: recordingDelegate)
}
func stopRecording(){
videoFileOutput.stopRecording()
print("🔴 RECORDING \(videoFileOutput.isRecording)")
}
}
Modify your code by this
struct VideoRecordingView: UIViewRepresentable {
#Binding var timeLeft: Int
#Binding var onComplete: Bool
#Binding var recording: Bool
func makeUIView(context: UIViewRepresentableContext<VideoRecordingView>) -> PreviewView {
let recordingView = PreviewView()
recordingView.onComplete = {
self.onComplete = true
}
recordingView.onRecord = { timeLeft, totalShakes in
self.timeLeft = timeLeft
self.recording = true
}
recordingView.onReset = {
self.recording = false
self.timeLeft = 30
}
return recordingView
}
func updateUIView(_ uiViewController: PreviewView, context: UIViewRepresentableContext<VideoRecordingView>) {
if recording {
uiViewController.start()
}
}
}
extension PreviewView: AVCaptureFileOutputRecordingDelegate{
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
print(outputFileURL.absoluteString)
}
}
class PreviewView: UIView {
private var captureSession: AVCaptureSession?
private var shakeCountDown: Timer?
let videoFileOutput = AVCaptureMovieFileOutput()
var recordingDelegate:AVCaptureFileOutputRecordingDelegate!
var recorded = 0
var secondsToReachGoal = 30
var onRecord: ((Int, Int)->())?
var onReset: (() -> ())?
var onComplete: (() -> ())?
init() {
super.init(frame: .zero)
var allowedAccess = false
let blocker = DispatchGroup()
blocker.enter()
AVCaptureDevice.requestAccess(for: .video) { flag in
allowedAccess = flag
blocker.leave()
}
blocker.wait()
if !allowedAccess {
print("!!! NO ACCESS TO CAMERA")
return
}
// setup session
let session = AVCaptureSession()
session.beginConfiguration()
let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video, position: .front)
guard videoDevice != nil, let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!), session.canAddInput(videoDeviceInput) else {
print("!!! NO CAMERA DETECTED")
return
}
session.addInput(videoDeviceInput)
session.commitConfiguration()
self.captureSession = session
}
override class var layerClass: AnyClass {
AVCaptureVideoPreviewLayer.self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
recordingDelegate = self
}
func start() {
startTimers()
if nil != self.superview {
self.videoPreviewLayer.session = self.captureSession
self.videoPreviewLayer.videoGravity = .resizeAspect
self.captureSession?.startRunning()
self.startRecording()
} else {
self.captureSession?.stopRunning()
}
}
private func onTimerFires(){
print("🟢 RECORDING \(videoFileOutput.isRecording)")
secondsToReachGoal -= 1
recorded += 1
onRecord?(secondsToReachGoal, recorded)
if(secondsToReachGoal == 0){
stopRecording()
shakeCountDown?.invalidate()
shakeCountDown = nil
onComplete?()
videoFileOutput.stopRecording()
}
}
func startTimers(){
if shakeCountDown == nil {
shakeCountDown = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] (timer) in
self?.onTimerFires()
}
}
}
func startRecording(){
captureSession?.addOutput(videoFileOutput)
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let filePath = documentsURL.appendingPathComponent("tempPZDC")
videoFileOutput.startRecording(to: filePath, recordingDelegate: recordingDelegate)
}
func stopRecording(){
videoFileOutput.stopRecording()
print("🔴 RECORDING \(videoFileOutput.isRecording)")
}
}

Show Selected ScrollView Item in Add New Item View SwiftUI

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

SwiftUI: How it refreshes view and why #Published ObservableObject properties works randomly

SwiftUI seems to me more and more confusing in the way it works.
At first glance it seams fast and easy to grasp. But if you add more and more views
something that seems to be simple starts to behave very odd and take many time to solve.
I have Input field with validation. This is customized input with that I can reuse in many places. But on different screens this can work totally different and totally unreliable.
View with form
struct LoginView {
#ObservedObject private var viewModel = LoginViewModel()
var body: some View {
VStack(spacing: 32) {
Spacer()
LabeledInput(label: "Email", input: self.$viewModel.email, isNuemorphic: true, rules: LoginFormRules.email, validation: self.$viewModel.emailValidation)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.autocapitalization(.none)
.frame(height: 50)
LabeledInput(label: "Password", isSecure: true, input: self.$viewModel.password, isNuemorphic: true, rules: LoginFormRules.password, validation: self.$viewModel.passwordValidation)
.textContentType(.password)
.keyboardType(.asciiCapable)
.autocapitalization(.none)
.frame(height: 50)
self.makeSubmitButton()
Spacer()
}
}
LabeledInput - resuable custom input view with validation support
struct LabeledInput: View {
// MARK: - Properties
let label: String?
let isSecure: Bool
// MARK: - Binding
#Binding var input: String
var isEditing: Binding<Bool>?
// MARK: - Actions
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
// MARK: - Validation
#ObservedObject var validator: FieldValidator<String>
// MARK: - Init
init(label: String? = nil,
isSecure: Bool = false,
input: Binding<String>,
isEditing: Binding<Bool>? = nil,
// validation
rules: [Rule<String>] = [],
validation: Binding<Validation>? = nil,
// actions
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = { }) {
self.label = label
self.isSecure = isSecure
self._input = input
self.isEditing = isEditing
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
self.validator = FieldValidator(input: input, rules: rules, validation: validation ?? .constant(Validation()))
}
var useUIKit: Bool {
self.isEditing != nil
}
var body: some View {
GeometryReader { geometry in
ZStack {
RoundedRectangle(cornerRadius: 4.0)
.stroke(lineWidth: 1)
.foregroundColor(!self.validator.validation.isEdited ? Color("LightGray")
: self.validator.validation.isValid ? Color("Green") : Color("Red"))
.frame(maxHeight: geometry.size.height)
.offset(x: 0, y: 16)
VStack {
HStack {
self.makeLabel()
.offset(x: self.isNuemorphic ? 0 : 16,
y: self.isNuemorphic ? 0 : 8)
Spacer()
}
Spacer()
}
self.makeField()
.frame(maxHeight: geometry.size.height)
.offset(x: 0, y: self.isNuemorphic ? 20 : 16)
.padding(10)
}
}
}
private func makeField() -> some View {
Group {
if useUIKit {
self.makeUIKitTextField(secure: self.isSecure)
} else {
if self.isSecure {
self.makeSecureField()
} else {
self.makeTextField()
}
}
}
}
private func makeLabel() -> some View {
Group {
if label != nil {
Text("\(self.label!.uppercased())")
.font(.custom("AvenirNext-Regular", size: self.isNuemorphic ? 13 : 11))
.foregroundColor(!self.validator.validation.isEdited ? Color("DarkBody")
: self.validator.validation.isValid ? Color("Green") : Color("Red"))
.padding(.horizontal, 8)
} else {
EmptyView()
}
}
}
private func makeSecureField() -> some View {
SecureField("", text: self.$input, onCommit: {
self.validator.onCommit()
self.onCommit()
})
.font(.custom("AvenirNext-Regular", size: 15))
.foregroundColor(Color("DarkBody"))
.frame(maxWidth: .infinity)
}
private func makeTextField() -> some View {
TextField("", text: self.$input, onEditingChanged: { editing in
self.onEditingChanged(editing)
self.validator.onEditing(editing)
if !editing { self.onCommit() }
}, onCommit: {
self.validator.onCommit()
self.onCommit()
})
.font(.custom("AvenirNext-Regular", size: 15))
.foregroundColor(Color("DarkBody"))
.frame(maxWidth: .infinity)
}
private func makeUIKitTextField(secure: Bool) -> some View {
let firstResponderBinding = Binding<Bool>(get: {
self.isEditing?.wrappedValue ?? false //?? self.isFirstResponder
}, set: {
//self.isFirstResponder = $0
self.isEditing?.wrappedValue = $0
})
return UIKitTextField(text: self.$input, isEditing: firstResponderBinding, font: UIFont(name: "AvenirNext-Regular", size: 15)!, textColor: UIColor(named: "DarkBody")!, placeholder: "", onEditingChanged: { editing in
self.onEditingChanged(editing)
self.validator.onEditing(editing)
}, onCommit: {
self.validator.onCommit()
self.onCommit()
})
}
}
And here is how I store model (input values and validation) in ObservableObject i.e. LoginViewModel.
final class LoginViewModel: ObservableObject {
// MARK: - Published
#Published var email: String = ""
#Published var password: String = ""
#Published var emailValidation: Validation = Validation(onEditing: true)
#Published var passwordValidation: Validation = Validation(onEditing: true)
#Published var validationErrors: [String]? = nil
#Published var error: DescribableError? = nil
}
When I use this code depending on how I create ViewModel (in LoginView property or injected to LoginView constructor) depending on view parent views (screens) it is embedded can work totally different can cause hours of debugging and unexpected behaviour.
Sometimes it seems that there is 1 ViewModel instance sometimes it seems that this instance is created with each View refresh
sometimes LabeledInput body is refreshing and validation colouring of label works corretly. Other times it seems it does not refresh at all and nothing happens
sometimes refreshes so often keyboard is immediately hiding
Other times there is no validation at all
Other times input is lost after exiting field or when rotating phone landscape to portrait
If there is some event that causes parent view refresh it can cause the inputs to lose data and validation.
Sometimes it refreshes to often other times it doesn't refresh at all as it should.
I've tried to add .id(UUID) , custom .id(refreshId) or other Equatable protocol implementations but it doesn't work as expected to be reusable customized input with validation reusable between multiple forms on multiple screens.
Here is simple validation struct
struct Validation {
let onEditing: Bool
init(onEditing: Bool = false) {
self.onEditing = onEditing
}
var isEdited: Bool = false
var errors: [String] = []
}
And here FieldValidator ObservableObject
class FieldValidator<T>: ObservableObject {
// MARK: - Properties
private let rules: [Rule<T>]
// MARK: - Binding
#Binding private var input: T
#Binding var validation: Validation
// MARK: - Init
init(input: Binding<T>, rules: [Rule<T>], validation: Binding<Validation>) {
#if DEBUG
print("[FieldValidator] init: \(input.wrappedValue)")
#endif
self._input = input
self.rules = rules
self._validation = validation
}
private var disposables = Set<AnyCancellable>()
}
// MARK: - Public API
extension FieldValidator {
func validateField() {
validation.errors = rules
.filter { !$0.isAsync }
.filter { !$0.validate(input) }
.map { $0.errorMessage() }
}
func validateFieldAsync() {
rules
.filter { $0.isAsync }
.forEach { rule in
rule.validateAsync(input)
.filter { valid in
!valid
}.sink(receiveValue: { _ in
self.validation.errors.append(rule.errorMessage())
})
.store(in: &disposables)
}
}
}
// MARK: - Helper Public API
extension FieldValidator {
func onEditing(_ editing: Bool) {
self.validation.isEdited = true
if editing {
if self.validation.onEditing {
self.validateField()
}
} else {
// on end editing
self.validateField()
self.validateFieldAsync()
}
}
func onCommit() {
self.validateField()
self.validateFieldAsync()
}
}
Rules are just subclasses of
class Rule<T> {
var isAsync: Bool { return false }
func validate(_ value: T) -> Bool { return false }
func errorMessage() -> String { return "" }
func validateAsync(_ value: T) -> AnyPublisher<Bool, Never> {
fatalError("Async validation is not implemented!")
}
}
UPDATE
Complete UIKitTextField example
#available(iOS 13.0, *)
struct UIKitTextField: UIViewRepresentable {
// MARK: - Observed
#ObservedObject private var keyboardEvents = KeyboardEvents()
// MARK: - Binding
#Binding var text: String
var isEditing: Binding<Bool>?
// MARK: - Actions
let onBeginEditing: () -> Void
let onEndEditing: () -> Void
let onEditingChanged: (Bool) -> Void
let onCommit: () -> Void
// MARK: - Proprerties
private let keyboardOffset: CGFloat
private let textAlignment: NSTextAlignment
private let font: UIFont
private let textColor: UIColor
private let backgroundColor: UIColor
private let contentType: UITextContentType?
private let keyboardType: UIKeyboardType
private let autocorrection: UITextAutocorrectionType
private let autocapitalization: UITextAutocapitalizationType
private let isSecure: Bool
private let isUserInteractionEnabled: Bool
private let placeholder: String?
public static let defaultFont = UIFont.preferredFont(forTextStyle: .body)
private var hasDoneToolbar: Bool = false
init(text: Binding<String>,
isEditing: Binding<Bool>? = nil,
keyboardOffset: CGFloat = 0,
textAlignment: NSTextAlignment = .left,
font: UIFont = UIKitTextField.defaultFont,
textColor: UIColor = .black,
backgroundColor: UIColor = .white,
contentType: UITextContentType? = nil,
keyboardType: UIKeyboardType = .default,
autocorrection: UITextAutocorrectionType = .default,
autocapitalization: UITextAutocapitalizationType = .none,
isSecure: Bool = false,
isUserInteractionEnabled: Bool = true,
placeholder: String? = nil,
hasDoneToolbar: Bool = false,
onBeginEditing: #escaping () -> Void = { },
onEndEditing: #escaping () -> Void = { },
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = { }) {
self._text = text
self.isEditing = isEditing
self.keyboardOffset = keyboardOffset
self.onBeginEditing = onBeginEditing
self.onEndEditing = onEndEditing
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
self.textAlignment = textAlignment
self.font = font
self.textColor = textColor
self.backgroundColor = backgroundColor
self.contentType = contentType
self.keyboardType = keyboardType
self.autocorrection = autocorrection
self.autocapitalization = autocapitalization
self.isSecure = isSecure
self.isUserInteractionEnabled = isUserInteractionEnabled
self.placeholder = placeholder
self.hasDoneToolbar = hasDoneToolbar
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.delegate = context.coordinator
textField.keyboardType = keyboardType
textField.textAlignment = textAlignment
textField.font = font
textField.textColor = textColor
textField.backgroundColor = backgroundColor
textField.textContentType = contentType
textField.autocorrectionType = autocorrection
textField.autocapitalizationType = autocapitalization
textField.isSecureTextEntry = isSecure
textField.isUserInteractionEnabled = isUserInteractionEnabled
//textField.placeholder = placeholder
if let placeholder = placeholder {
textField.attributedPlaceholder = NSAttributedString(
string: placeholder,
attributes: [
NSAttributedString.Key.foregroundColor: UIColor.lightGray
])
}
textField.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged(_:)), for: .editingChanged)
keyboardEvents.didShow = {
if textField.isFirstResponder {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(350)) {
textField.adjustScrollView(offset: self.keyboardOffset, animated: true)
}
}
}
if hasDoneToolbar {
textField.addDoneButton {
print("Did tap Done Toolbar button")
textField.resignFirstResponder()
}
}
return textField
}
func updateUIView(_ textField: UITextField, context: Context) {
textField.text = text
if let isEditing = isEditing {
if isEditing.wrappedValue {
textField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
}
}
final class Coordinator: NSObject, UITextFieldDelegate {
let parent: UIKitTextField
init(_ parent: UIKitTextField) {
self.parent = parent
}
#objc func valueChanged(_ textField: UITextField) {
parent.text = textField.text ?? ""
parent.onEditingChanged(true)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.onBeginEditing()
parent.onEditingChanged(true)
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
//guard textField.text != "" || parent.shouldCommitIfEmpty else { return }
DispatchQueue.main.async {
self.parent.isEditing?.wrappedValue = false
}
parent.text = textField.text ?? ""
parent.onEditingChanged(false)
parent.onEndEditing()
parent.onCommit()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
parent.isEditing?.wrappedValue = false
textField.resignFirstResponder()
parent.onCommit()
return true
}
}
}
extension UIView {
func adjustScrollView(offset: CGFloat, animated: Bool = false) {
if let scrollView = findParent(of: UIScrollView.self) {
let contentOffset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + offset)
scrollView.setContentOffset(contentOffset, animated: animated)
} else {
print("View is not in ScrollView - do not adjust content offset")
}
}
}
Here is sample EmailRule implementation
class EmailRule : RegexRule {
static let regex = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
public convenience init(message : String = "Email address is invalid"){
self.init(regex: EmailRule.regex, message: message)
}
override func validate(_ value: String) -> Bool {
guard value.count > 0 else { return true }
return super.validate(value)
}
}

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
}

Resources