#FetchRequest predicate is ignored if the context changes - swift

I have a simple SwiftUI application with CoreData and two views. One view displays all "Place" objects. You can create new places and you can show the details for the place.
Inside the second view you can add "PlaceItem"s to a place.
The problem is that, once a new "PlaceItem" is added to the viewContext, the #NSFetchRequest seems to forget about its additional predicates, which I set in onAppear. Then every place item is shown inside the details view. Once I update the predicate manually (the refresh button), only the items from the selected place are visible again.
Any idea how this can be fixed? Here's the code for my two views:
struct PlaceView: View {
#FetchRequest(sortDescriptors: []) private var places: FetchedResults<Place>
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
NavigationView {
List(places) { place in
NavigationLink {
PlaceItemsView(place: place)
} label: {
Text(place.name ?? "")
}
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let place = Place(context: viewContext)
place.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
}
.navigationTitle("Places")
}
}
struct PlaceItemsView: View {
#ObservedObject var place: Place
#State var searchText = ""
#FetchRequest(sortDescriptors: []) private var items: FetchedResults<PlaceItem>
#Environment(\.managedObjectContext) private var viewContext
func updatePredicate() {
var predicates = [NSPredicate]()
predicates.append(NSPredicate(format: "place == %#", place))
if !searchText.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS %#", searchText))
}
items.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
}
var body: some View {
NavigationView {
List(items) { item in
Text(item.name ?? "");
}
}
.onAppear(perform: updatePredicate)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let item = PlaceItem(context: viewContext)
item.place = place
item.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: updatePredicate) {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search or add articles …"
)
.onAppear(perform: updatePredicate)
.onChange(of: searchText, perform: { _ in
updatePredicate()
})
.navigationTitle(place.name ?? "")
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
NavigationView {
PlaceView()
}
}
}
Thanks!

Rather than relying on onAppear, you might have more luck defining your fetchRequest in PlaceItemView’s initializer, e.g.:
struct PlaceItemsView: View {
#ObservedObject private var place: Place
#FetchRequest private var items: FetchedResults<PlaceItem>
init(place: Place) {
self.place = place
self._items = FetchRequest(
entity: PlaceItem.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "place == %#", place)
)
}

It's best to break Views up by the data they need, its called having a tighter invalidation and solves your problem of updating the fetch request too! e.g.
struct PlaceItemsView {
#FetchRequest private var items: FetchedResults<PlaceItem>
var body: some View {
List(items) { item in
Text(item.name ?? "")
}
}
}
// body called when either searchText is different or its a different place (not when the place's properties change because no need to)
struct SearchPlaceView {
let searchText: String
let place: Place
var predicate: NSPredicate {
var predicates = [NSPredicate]()
predicates.append(NSPredicate(format: "place == %#", place))
if !searchText.isEmpty {
predicates.append(NSPredicate(format: "name CONTAINS %#", searchText))
}
return NSCompoundPredicate(type: .and, subpredicates: predicates)
}
var body: some View {
PlaceItemsView(items:FetchRequest<PlaceItem>(sortDescriptors:[], predicate:predicate))
}
}
// body called when either the place has a change or search text changes
struct PlaceItemsView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var place: Place
#State var searchText = ""
var body: some View {
NavigationView {
SearchPlaceView(searchTest:searchText, place: place)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
let item = PlaceItem(context: viewContext)
item.place = place
item.name = NSUUID().uuidString
try! viewContext.save()
} label: {
Label("Add", systemImage: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: updatePredicate) {
Label("Refresh", systemImage: "arrow.clockwise")
}
}
}
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search or add articles …"
)
.navigationTitle(place.name ?? "")
}
}
Always try to break the Views up by the smallest data their body needs, try to not think about breaking them up by screens or areas of a screen which is a very common mistake.

import UIKit
import CoreData
class LoginViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
//MARK: - Outlets
#IBOutlet weak var LogInDataTableView: UITableView!
#IBOutlet weak var btnLogIn: UIButton!
#IBOutlet weak var btnAdd: UIButton!
//MARK: - Variables
var fetchedCoreData = [NSManagedObject]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var name = String()
let placeHolderArray = ["username", "password"]
var isLogin = false
//MARK: - Methods and other functionalities
override func viewDidLoad() {
super.viewDidLoad()
btnLogIn.layer.cornerRadius = 20.0
LogInDataTableView.layer.cornerRadius = 20.0
LogInDataTableView.layer.borderWidth = 2.0
LogInDataTableView.layer.borderColor = UIColor.blue.cgColor
let tap = UITapGestureRecognizer(target: self, action: #selector(UIInputViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
//MARK: - dismiss keyboard when tap on screen
#objc func dismissKeyboard() {
//Causes the view (or one of its embedded text fields) to resign the first responder status.
view.endEditing(true)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
fetchRequest()
}
//MARK: - fetch saved data from coredata
func fetchRequest() {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "EntityName")
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
fetchedCoreData = result as! [NSManagedObject]
} catch {
print("error while fetch data from database")
}
}
//MARK: - login button click with validation
#IBAction func btnLogInClick(_ sender: Any) {
if loginData[0].isEmpty {
let alert = UIAlertController(title: "Alert", message: "Please enter username", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else if loginData[1].isEmpty {
let alert = UIAlertController(title: "Alert", message: "Please enter password", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else if loginData[2].isEmpty{
let alert = UIAlertController(title: "Alert", message: "Please enter id", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
} else {
let registeredUser = clinicData.contains { objc in
objc.value(forKey: "username") as! String == loginData[0] && objc.value(forKey: "password") as! String == loginData[1]
}
if registeredUser == true {
for data in clinicData {
if data.value(forKey: "username") as! String == loginData[0] && data.value(forKey: "password") as! String == loginData[1] {
clinicName = data.value(forKey: "clinic") as! String
let addClinicVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "HomeScreenViewController") as! HomeScreenViewController
addClinicVC.clinicName = clinicName
self.navigationController?.pushViewController(addClinicVC, animated: true)
}
}
} else {
//MARK: - alert
let alert = UIAlertController(title: "Alert", message: "username and password mismatch", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(okAction)
present(alert, animated: true, completion: nil)
}
}
}
#IBAction func didTapAddClinic(_ sender: UIButton) {
let addVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "NewScreenViewController") as! NewScreenViewController
self.navigationController?.pushViewController(addVC, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return placeHolderArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LoginTableViewCell", for: indexPath) as! LoginTableViewCell
cell.txtLoginData.tag = indexPath.row
cell.txtLoginData.placeholder = placeHolderArray[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
}

Related

Persistently store list items added by user in SwiftUI

I am very new to swift (as in I started today) and I have code that allows the user to add items to a list:
private func onAdd() {
let alert = UIAlertController(title: "Enter a name for your plant", message: "Make sure it's descriptive!", preferredStyle: .alert)
alert.addTextField { (textField) in
textField.placeholder = "Enter here"
}
alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
let textField = alert.textFields![0] as UITextField
plantName2 = textField.text ?? "Name"
appendItem()
})
showAlert(alert: alert)
}
private func appendItem() {
items.append(Item(title: plantName2))
}
and
struct HomePage: View {
#State var plantName2: String = ""
#State private var items: [Item] = []
#State private var editMode = EditMode.inactive
private static var count = 0
var body: some View {
NavigationView {
List {
Section(header: Text("My Plants")) {
ForEach(items) { item in
NavigationLink(destination: PlantView(plantName3: item.title)) {
Text(item.title)
}
}
.onDelete(perform: onDelete)
.onMove(perform: onMove)
.onInsert(of: [String(kUTTypeURL)], perform: onInsert)
}
}
.listStyle(InsetGroupedListStyle()) // or GroupedListStyle
.navigationBarTitle("Plantify")
.navigationBarTitleTextColor(CustomColor.pastelGreen)
.navigationBarItems(leading: EditButton().accentColor(CustomColor.pastelGreen), trailing: addButton)
.environment(\.editMode, $editMode)
}
}
and I want the entries the user adds to be saved in persistent storage. I've looked at the docs for persistent storage and am a bit confused. Is it even possible with the code I have?
Thanks!
Basically you can save the data as String array in UserDefaults
func save() {
UserDefaults.standard.set(items.map(\.title), forKey: "items")
}
func load() {
let savedItems = UserDefaults.standard.stringArray(forKey: "items") ?? []
items = savedItems.map(Item.init)
}
and call load in .onAppear.
However if there is a huge amount of items consider a file in the Documents folder or Core Data

Get Live Text when press button

UIAction.captureTextFromCamera(responder: context.coordinator, identifier: nil)
This method must be placed in a Menu before it can be used. What if I want to call this method when I press the Button?
And I wanna use it in the SwiftUI, like this
#State var text = ""
Text(text)
Menu {
Button(action: {
// TODO: support live text
}) {
Label("Live Text", systemImage: "camera.viewfinder")
}
} label: {
Text("Hi")
}
import SwiftUI
#available(iOS 15.0, *)
struct ScanTextView: View {
#State var text: String = ""
#State var message: String = ""
var body: some View {
VStack {
Text(message)
HStack{
TextField("Enter text",text: $text)
ScanTextView_UI(text: $text, type: .buttonMenu,imageSystemName: "ellipsis.circle", additionalActions: [
//https://developer.apple.com/documentation/uikit/uiaction
UIAction(title: "Refresh") { (action) in
//Just sample
self.message = "refresh"
},
UIAction(title: "Anything", image: UIImage(systemName: "checkmark")) { (action) in
//Just sample
self.message = "anything"
},
UIAction(title: "Clear message") { (action) in
//Just sample
self.message = ""
}
])
}.border(Color.red)
TextField("Enter text",text: $text)
.border(Color.yellow)
.toolbar(content: {
ToolbarItem(placement: .keyboard, content: {
ScanTextView_UI(text: $text, type: .buttonMenu)
})
})
ScanTextView_UI(text: $text, type: .textField, additionalActions: [
UIAction(title: "Anything", image: UIImage(systemName: "checkmark")) { (action) in
//Just sample
self.message = "anything"
}
]).border(Color.orange)
HStack{
TextField("Enter text",text: $text)
ScanTextView_UI(text: $text, type: .button)
}.border(Color.green)
}
}
}
#available(iOS 15.0, *)
struct ScanTextView_UI: UIViewRepresentable {
#Binding var text: String
let type: Types
var imageSystemName: String = "text.viewfinder"
var additionalActions: [UIAction] = []
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> some UIView {
let textFromCamera = UIAction.captureTextFromCamera(responder: context.coordinator, identifier: nil)
let image = UIImage(systemName: imageSystemName)
//button
let button = UIButton(frame: .zero, primaryAction: textFromCamera)
button.setImage(image, for: .normal)
//buttonMenu
// A UIButton can hold the menu, it is a long press to get it to come up
let buttonMenu = UIButton()
var temp: [UIAction] = [textFromCamera]
for elem in additionalActions{
temp.append(elem)
}
let cameraMenu = UIMenu(children: temp)
buttonMenu.setImage(image, for: .normal)
buttonMenu.menu = cameraMenu
//TextField
// Or a textField
let toolbarItem = UIBarButtonItem(title: nil, image: image, primaryAction: textFromCamera, menu: nil)
var temp2: [UIBarButtonItem] = [toolbarItem]
for elem in additionalActions{
temp2.append(UIBarButtonItem(title: elem.title, image: elem.image, primaryAction: elem, menu: nil))
}
let textField = UITextField()
let bar = UIToolbar()
bar.items = temp2
bar.sizeToFit()
textField.inputAccessoryView = bar
textField.placeholder = "Enter text"
textField.delegate = context.coordinator
textField.text = text
var result: UIView = UIView()
switch type {
case .textField:
result = textField
case .buttonMenu:
result = buttonMenu
case .button:
if !additionalActions.isEmpty{
result = buttonMenu
}else{
result = button
}
}
return result
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if uiView is UITextField{
(uiView as! UITextField).text = text
}
}
//Making the Coordinator a UIResponder as! UIKeyInput gives access to the text
class Coordinator: UIResponder, UIKeyInput, UITextFieldDelegate{
var hasText: Bool{
!parent.text.isEmpty
}
let parent: ScanTextView_UI
init(_ parent: ScanTextView_UI){
self.parent = parent
}
func insertText(_ text: String) {
//Update the #Binding
parent.text = text
}
func deleteBackward() { }
// MARK: UITextFieldDelegate
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
enum Types{
case textField
case buttonMenu
case button
}
}
#available(iOS 15.0, *)
struct ActionMenuView_Previews: PreviewProvider {
static var previews: some View {
ScanTextView()
}
}

How to save position in an array and a view count using UserDefaults?

first time poster here. I want to save the position in an array and a view count individually for each item in the array. I have a struct which handles the data here;
import Foundation
struct Vocab {
let vocabTitle: String
let vocabHiragana: String
let englishTranslation: String
}
I then call it using a separate swift file with a struct:
import Foundation
var vocabNumber = 0
struct VocabBuilder {
let testVocab = [ Vocab(vocabTitle: "上達", vocabHiragana: "じょうたつ", englishTranslation: "Improvement"),
Vocab(vocabTitle: "乗り越える", vocabHiragana: "のりこえる", englishTranslation: "To Push Through"),
Vocab(vocabTitle: "耐久性", vocabHiragana: "たいきゅうせい", englishTranslation: "Durability"),
Vocab(vocabTitle: "促す", vocabHiragana: "うながす", englishTranslation: "To doubt"),
Vocab(vocabTitle: "一律", vocabHiragana: "いちりつ", englishTranslation: "Across the board"),
Vocab(vocabTitle: "押し切り", vocabHiragana: "おしきり", englishTranslation: "Objection"),
Vocab(vocabTitle: "打ち込む", vocabHiragana: "うちこむ", englishTranslation: "To input") ,
Vocab(vocabTitle: "溶け込む", vocabHiragana: "とけこむ", englishTranslation: "To get used to"),
Vocab(vocabTitle: "放り込む", vocabHiragana: "ほうりこむ", englishTranslation: "Throw into (something)"),
Vocab(vocabTitle: "後継者", vocabHiragana: "こうけいしゃ", englishTranslation: "Successor") ]
func getVocab() -> String {
return testVocab[vocabNumber].vocabTitle
}
func getHiragana() -> String {
return testVocab[vocabNumber].vocabHiragana
}
func getEnglishTranslation() -> String {
return testVocab[vocabNumber].englishTranslation
}
mutating func nextVocab() {
NSLog("Before", "\(vocabNumber)")
vocabNumber = (vocabNumber + Int.random(in: 0...9)) % testVocab.count
NSLog("After", "\(vocabNumber)")
}
}
The problem I have is that in my main ViewController is that I want to save the position in an array including all the functions that return strings into the ViewController:
import UIKit
class ViewController: UIViewController {
var greySquare: UIView!
var answerBox: UILabel!
var continueButton: UIButton!
var inputButton: UIButton!
var rightWrongAnswerBox: UILabel!
let rightWrongText = String()
var englishTranslationBox: UILabel!
var hiraganaBox: UILabel!
var viewCountBox: UILabel!
var userInputs: String?
var viewedVocab = [String]()
var pictureViewCount: Int = 0
var vocabInfo = VocabBuilder() //create var to handle the struct info
override func viewDidLoad() {
super.viewDidLoad()
func loadView() {
view = UIView()
view.backgroundColor = .white
// initialised views for each box here
NSLayoutConstraint.activate([
// had the constraints here for the views.
])
loadView()
loadUI()
let defaults = UserDefaults.standard
//let savedData = pictureViewCount
if let savedViewCount = defaults.value(forKey: "picVC") as? Int {
print("savedValue: \(savedViewCount + 1)")
}
}
func loadUI() {
answerBox.text = vocabInfo.getVocab()
englishTranslationBox.text = vocabInfo.getEnglishTranslation()
hiraganaBox.textColor = .white
hiraganaBox.text = vocabInfo.getHiragana()
viewCountBox.text = ("View Count: \(pictureViewCount)"}
}
rightWrongAnswerBox.text = "Try and Remember!"
rightWrongAnswerBox.textColor = .black
pictureViewCount += 1
save()
}
func submit(answer: String) {
if hiraganaBox.text == answer {
let ac = UIAlertController(title: "Correct!", message: "Go to the next word!", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
hiraganaBox.textColor = .black
rightWrongAnswerBox.text = "Correct!"
rightWrongAnswerBox.textColor = .green
} else if hiraganaBox.text != answer {
let ac = UIAlertController(title: "Incorrect!", message: "Give it another go.", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "OK", style: .default))
present(ac, animated: true)
rightWrongAnswerBox.text = "Incorrect"
rightWrongAnswerBox.textColor = .red
}
}
#objc func answerAttempt (_ sender: UIButton) {
let ac = UIAlertController(title: "What is this word in Hiragana?", message: "Enter your answer into the box below", preferredStyle: .alert)
ac.addTextField()
let submitAction = UIAlertAction(title: "Submit", style: .default){
[weak ac] action in// make a closure to handle text field.
guard let answer = ac?.textFields?[0].text else { return }
self.submit(answer: answer)
}
ac.addAction(submitAction)
present(ac, animated: true)
}
#objc func continueToNextWord (_ sender: UIButton) {
vocabInfo.nextVocab()
loadUI()
}
func save(){
//let jsonEncoder = JSONEncoder()
//if let saveNumber = try?jsonEncoder.encode(pictureViewCount) {
// let savedWord = try?jsonEncoder.encode(vocabInfo.getVocab())
let defaults = UserDefaults.standard
let savedData = pictureViewCount
let currentCount = UserDefaults.standard.integer(forKey: "picVC")
UserDefaults.standard.setValue(currentCount + 1, forKey: "picVC")
defaults.set(saveData, forKey: "pictureViewCount")
defaults.setValue(savedWord, forKey: "savedWord")
} else {
print("Failed to save View Count.")
}
}
}
What I've tried:
The issue I have is that I'm just not sure after looking at tutorials where saving arrays. What I've tried is using JSON to save and encode and decode the user defaults to keep the position in the array loaded up when the app is closed and loaded again. Also I tried to increase the picture view count for each item in the array, which is randomly loaded up and save it as an integer but it just increases the view count when the continueToNextWord function is called.
View Controller as it is now so when I press the next word button I want it to save the view count on the randomly shown words and assign it to that item.
I'm gonna try some different ways to get the UserDefaults working but any help will be appreciated.
If its not clear I can also use DMs.

How i can push new view when i use coordinator with mvvm-c Rxswift?

i works with MVVM-C (Rxswift) i have to create homeVC , HomeViewModel and HomeCoordinator
when i try to connect HomeCoordinator with HomeViewModel to push new VC to Coordinator it's not take any action
Can any one help me with example or explain how to solve this problem?
Maybe this example will help you:
//
// ExampleLiftData.swift
//
// Created by Daniel Tartaglia on 2/18/20.
// Copyright © 2020 Daniel Tartaglia. MIT License.
//
import UIKit
import RxSwift
import RxCocoa
import RxEnumKit
import EnumKit
// MARK: - ExamplePresenter
func presentExample(window: UIWindow) {
let value = PublishSubject<String>()
let storyboard = UIStoryboard(name: "Example", bundle: nil)
let controller = storyboard.instantiateInitialViewController() as! ExampleViewController
let action = controller.install(viewModel: exampleViewModel(value: value.asObservable()))
_ = action
.filter(case: ExampleAction.showRequest)
.flatMap { [unowned controller] _ in
presentRequest(on: controller, title: "", message: "Add how much?")
}
.bind(to: value)
window.rootViewController = controller
}
enum ExampleAction: CaseAccessible {
case showRequest
}
// MARK: - ExampleViewModel
func exampleViewModel(value: Observable<String>) -> (ExampleInput) -> (ExampleOutput, Observable<ExampleAction>) {
return { input in
let state = value
.compactMap { Int($0) }
.scan(0, accumulator: +)
.startWith(0)
return (
ExampleOutput(text: state.map { "\($0)" }),
input.buttonTapped.map { ExampleAction.showRequest }
)
}
}
struct ExampleInput {
let buttonTapped: Observable<Void>
}
// MARK: - ExampleView
class ExampleViewController: UIViewController, HasViewModel {
#IBOutlet weak var button: UIButton!
#IBOutlet weak var label: UILabel!
var buildOutput: (ExampleInput) -> ExampleOutput = { _ in fatalError() }
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let input = ExampleInput(
buttonTapped: button.rx.tap.asObservable()
)
let output = buildOutput(input)
output.text.bind(to: label.rx.text).disposed(by: disposeBag)
}
}
struct ExampleOutput {
let text: Observable<String>
}
// MARK: - AlertPresenter
func presentRequest(on root: UIViewController, title: String, message: String) -> Observable<String> {
let result = PublishSubject<String>()
let controller = UIAlertController(title: title.isEmpty ? nil : title, message: message.isEmpty ? nil : message, preferredStyle: .alert)
controller.addTextField(configurationHandler: { $0.keyboardType = .numberPad })
let ok = UIAlertAction(title: "OK", style: .default, handler: { [unowned controller] _ in
result.onNext(controller.textFields?[0].text ?? "")
result.onCompleted()
})
let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { _ in
result.onCompleted()
})
controller.addAction(ok)
controller.addAction(cancel)
root.present(controller, animated: true)
return result
}
Learn more at https://github.com/danielt1263/RxEarthquake

UIAlertController Already presenting (null)

I have an app where at first a map is shown with all companies close to my position. Main screen has a search button where I can click and filter results according to my specific needs
The connection between Mapa View Controller and Filtrar View Controller was built based on a custom protocol (according to https://medium.com/#jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef).
All the communication works fine and I'm able to filter and present companies that meet the criteria. My issue is when the filter returns nothing (there are no companies that meet the criteria). When that happen I wanted to present a UIAltert as an advice to the end user.
Alert is triggered but I get an error message "Attempt to present on which is already presenting (null)".
I tried one of the suggestions from What is the best way to check if a UIAlertController is already presenting?
if self.presentedViewController == nil {
// do your presentation of the UIAlertController
// ...
} else {
// either the Alert is already presented, or any other view controller
// is active (e.g. a PopOver)
// ...
let thePresentedVC : UIViewController? = self.presentedViewController as UIViewController?
if thePresentedVC != nil {
if let thePresentedVCAsAlertController : UIAlertController = thePresentedVC as? UIAlertController {
// nothing to do , AlertController already active
// ...
print("Alert not necessary, already on the screen !")
} else {
// there is another ViewController presented
// but it is not an UIAlertController, so do
// your UIAlertController-Presentation with
// this (presented) ViewController
// ...
thePresentedVC!.presentViewController(...)
print("Alert comes up via another presented VC, e.g. a PopOver")
}
}
}
I see "Alert comes up via another presented VC, e.g. a PopOver" printed but I don't know how to use thePresentedVC!.presentViewController(...).
I'm using XCode 11.2.1 and IOS 11.
My detailed code is
Alert Class
import UIKit
class Alerta {
var titulo : String
var mensagem : String
init(titulo: String, mensagem: String) {
self.titulo = titulo
self.mensagem = mensagem
}
func getAlerta() -> UIAlertController {
let alerta = UIAlertController(title: titulo, message: mensagem, preferredStyle: .alert)
let acaoCancelar = UIAlertAction(title: "Ok", style: .cancel, handler: nil)
alerta.addAction(acaoCancelar)
return alerta
}
}
Mapa Class
import UIKit
import MapKit
import ProgressHUD
class MapaViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, FiltroVCDelegate {
#IBOutlet weak var mapa: MKMapView!
var gerenciadorLocalizacao = CLLocationManager()
var anotacaoArray = [MinhaAnotacao]()
// var annotationSearch: [MinhaAnotacao] = [] // Filtered annotation
var anotacao = MinhaAnotacao()
var searchArr: [String] = []
var searching = false
var todasAnotacoes: [(objLat: CLLocationDegrees, objLong: CLLocationDegrees, objName: String, objDesc: String, objId: String, objTel1: String, objTel2: String, objEsp1: String, objEsp2: String, objEst: String, objCid: String)] = []
var clinicasR: [(objLat: CLLocationDegrees, objLong: CLLocationDegrees, objName: String, objDesc: String, objId: String, objTel1: String, objTel2: String, objEsp1: String, objEsp2: String, objEst: String, objCid: String)] = []
#IBOutlet weak var adicionarOutlet: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
mapa.register(MyAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
// Retrieving Logger user data and hidding "add" button if applicable
ProgressHUD.show("Carregando...")
var searching = false
// Map - User location
self.mapa.delegate = self
self.gerenciadorLocalizacao.delegate = self
self.gerenciadorLocalizacao.desiredAccuracy = kCLLocationAccuracyBest
self.gerenciadorLocalizacao.requestWhenInUseAuthorization()
self.gerenciadorLocalizacao.startUpdatingLocation()
ProgressHUD.dismiss()
}
// add annotations to the map
func addAnnotationsToMap() {
anotacaoArray = []
self.mapa.removeAnnotations(mapa.annotations)
if searching {
for oneObject in self.clinicasR {
let umaAnotacao = MinhaAnotacao()
let oneObjLoc: CLLocationCoordinate2D = CLLocationCoordinate2DMake(oneObject.objLat, oneObject.objLong)
umaAnotacao.coordinate = oneObjLoc
umaAnotacao.title = oneObject.objName
umaAnotacao.subtitle = oneObject.objDesc
umaAnotacao.identicadorMapa = oneObject.objId
umaAnotacao.telefone = oneObject.objTel1
umaAnotacao.telefone2 = oneObject.objTel2
umaAnotacao.especialidade1 = oneObject.objEsp1
umaAnotacao.especialidade2 = oneObject.objEsp2
umaAnotacao.estado = oneObject.objEst
umaAnotacao.cidade = oneObject.objCid
umaAnotacao.endereco = oneObject.objDesc
umaAnotacao.latitude = String(oneObject.objLat)
umaAnotacao.longitude = String(oneObject.objLong)
self.anotacaoArray.append(umaAnotacao)
}
} else {
for oneObject in self.todasAnotacoes {
let umaAnotacao = MinhaAnotacao()
let oneObjLoc: CLLocationCoordinate2D = CLLocationCoordinate2DMake(oneObject.objLat, oneObject.objLong)
umaAnotacao.coordinate = oneObjLoc
umaAnotacao.title = oneObject.objName
umaAnotacao.subtitle = oneObject.objDesc
umaAnotacao.identicadorMapa = oneObject.objId
umaAnotacao.telefone = oneObject.objTel1
umaAnotacao.telefone2 = oneObject.objTel2
umaAnotacao.especialidade1 = oneObject.objEsp1
umaAnotacao.especialidade2 = oneObject.objEsp2
umaAnotacao.estado = oneObject.objEst
umaAnotacao.cidade = oneObject.objCid
umaAnotacao.endereco = oneObject.objDesc
umaAnotacao.latitude = String(oneObject.objLat)
umaAnotacao.longitude = String(oneObject.objLong)
self.anotacaoArray.append(umaAnotacao)
}
}
self.mapa.addAnnotations(self.anotacaoArray)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "clinicaDetailsSegue" {
let clinicasDetailsViewController = segue.destination as! ClinicasDetailsViewController
clinicasDetailsViewController.identificador = self.anotacao.identicadorMapa
} else if segue.identifier == "searchSegue" {
if let nav = segue.destination as? UINavigationController, let filterVC = nav.topViewController as? FiltrarViewController {
filterVC.delegate = self
}
}
}
func search(searchAnnotation: [MinhaAnotacao], searchArr: [String]) -> [MinhaAnotacao] {
var searchArr = searchArr
// base case - no more searches - return clinics found
if searchArr.count == 0 {
return searchAnnotation
}
// itterative case - search clinic with next search term and pass results to next search
let foundAnnotation = searchAnnotation.filter { item in
(item.title!.lowercased() as AnyObject).contains(searchArr[0]) ||
item.especialidade1.lowercased().contains(searchArr[0]) ||
item.especialidade2.lowercased().contains(searchArr[0]) ||
item.cidade.lowercased().contains(searchArr[0]) ||
item.estado.lowercased().contains(searchArr[0]) ||
item.latitude.contains(searchArr[0]) || item.longitude.contains(searchArr[0]) || item.endereco.contains(searchArr[0])
}
// remove completed search and call next search
searchArr.remove(at: 0)
return search(searchAnnotation: foundAnnotation, searchArr: searchArr)
}
// From Custom Protocol
func dadosEscolhidos(nomeClinicaFiltro: String, estadoClinicaFiltro: String, cidadeClinicaFiltro: String, especialidade1ClinicaFiltro: String, especialidade2ClinicaFiltro: String) {
searchArr = []
clinicasR = []
searchArr = ["\(nomeClinicaFiltro.lowercased())","\(estadoClinicaFiltro.lowercased())", "\(cidadeClinicaFiltro.lowercased())", "\(especialidade1ClinicaFiltro.lowercased())", "\(especialidade2ClinicaFiltro.lowercased())"]
searchArr = searchArr.filter({ $0 != ""})
let annotationSearch = search(searchAnnotation: anotacaoArray, searchArr: searchArr) // Filtered Clinicas
if annotationSearch.count > 0 {
for i in 0...annotationSearch.count - 1 {
self.clinicasR.append((objLat: Double(annotationSearch[i].latitude)!, objLong: Double(annotationSearch[i].longitude)!, objName: annotationSearch[i].title!, objDesc: annotationSearch[i].endereco, objId: annotationSearch[i].identicadorMapa, objTel1: annotationSearch[i].telefone, objTel2: annotationSearch[i].telefone2, objEsp1: annotationSearch[i].especialidade1, objEsp2: annotationSearch[i].especialidade2, objEst: annotationSearch[i].estado, objCid: annotationSearch[i].cidade))
}
searching = true
addAnnotationsToMap()
} else {
if self.presentedViewController == nil {
let alerta = Alerta(titulo: "Erro", mensagem: "Nenhuma clínica atende ao filtro definido")
self.present(alerta.getAlerta(), animated: true, completion: nil)
print( "e Aqui, chegou? \(annotationSearch.count)")
} else {
// either the Alert is already presented, or any other view controller
// is active (e.g. a PopOver)
// ...
let thePresentedVC : UIViewController? = self.presentedViewController as UIViewController?
if thePresentedVC != nil {
if let thePresentedVCAsAlertController : UIAlertController = thePresentedVC as? UIAlertController {
// nothing to do , AlertController already active
// ...
print("Alert not necessary, already on the screen !")
} else {
// there is another ViewController presented
// but it is not an UIAlertController, so do
// your UIAlertController-Presentation with
// this (presented) ViewController
// ...
//let alerta = Alerta(titulo: "Erro", mensagem: "Nenhuma clínica atende ao filtro definido")
//thePresentedVC!.presentedViewController(alerta)
print("Alert comes up via another presented VC, e.g. a PopOver")
}
}
}
}
}
}
Filtrar View Controller
import UIKit
import ProgressHUD
protocol FiltroVCDelegate: class {
func dadosEscolhidos(nomeClinicaFiltro: String, estadoClinicaFiltro: String, cidadeClinicaFiltro: String, especialidade1ClinicaFiltro: String, especialidade2ClinicaFiltro: String)
}
class FiltrarViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
weak var delegate: FiltroVCDelegate?
var nomeSelecionado = ""
var estadosJSON = [Estado]()
var cidades = [Cidade]()
var estado : Estado? // Selected State identifier
var cidade : Cidade? // Selected City identifier
var estadoSelecionado = "" // Selected State
var cidadeSelecionada = "" // Selected City
var especialidadesJSON = [Especialidade]()
var especialidades2 = [Especialidade2]()
var especialidade1 : Especialidade? // Selected Specialty1 identifier
var especialidade2 : Especialidade2? // Selected Specialty2 identifier
var especialidade1Selecionada = ""
var especialidade2Selecionada = ""
let fontName = "HelveticaNeue"
var searchArr = [String]()
#IBOutlet weak var nomeClinica: UITextField!
#IBOutlet weak var especialidadeLabel: UILabel!
#IBOutlet weak var estadoClinicaPicker: UIPickerView!
#IBOutlet weak var especialidade1Picker: UIPickerView!
#IBOutlet weak var especialidade2Picker: UIPickerView!
override func viewDidLoad() {
ProgressHUD.show("Carregando...")
readJsonEstados()
readJsonEspecialidades()
super.viewDidLoad()
nomeClinica.text = ""
especialidadeLabel.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
ProgressHUD.dismiss()
}
#IBAction func aplicarFiltro(_ sender: Any) {
if nomeClinica.text == nil {
nomeClinica.text = ""
}
delegate?.dadosEscolhidos(nomeClinicaFiltro: nomeClinica.text!, estadoClinicaFiltro: estadoSelecionado, cidadeClinicaFiltro: cidadeSelecionada, especialidade1ClinicaFiltro: especialidade1Selecionada, especialidade2ClinicaFiltro: especialidade2Selecionada)
navigationController?.dismiss(animated: true)
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
especialidade1Picker.reloadComponent(0)
especialidade2Picker.reloadComponent(0)
estadoClinicaPicker.reloadAllComponents()
if pickerView == estadoClinicaPicker {
if component == 0 {
self.estado = self.estadosJSON[row]
self.cidades = self.estadosJSON[row].cidades
estadoClinicaPicker.reloadComponent(1)
estadoClinicaPicker.selectRow(0, inComponent: 1, animated: true)
} else {
self.cidade = self.cidades[row]
estadoClinicaPicker.reloadAllComponents()
}
} else if pickerView == especialidade1Picker {
self.especialidade1 = self.especialidadesJSON[row]
self.especialidades2 = self.especialidadesJSON[row].especialidade2
especialidade1Picker.reloadComponent(0)
especialidade2Picker.reloadComponent(0)
especialidade2Picker.selectRow(0, inComponent: 0, animated: true)
} else if pickerView == especialidade2Picker {
self.especialidade2 = self.especialidades2[row]
especialidade1Picker.reloadComponent(0)
especialidade2Picker.reloadComponent(0)
}
let estadoIndiceSelecionado = estadoClinicaPicker.selectedRow(inComponent: 0)
let cidadeIndiceSelecionada = estadoClinicaPicker.selectedRow(inComponent: 1)
let especialidade1IndiceSelecionado = especialidade1Picker.selectedRow(inComponent: 0)
let especialidade2IndiceSelecionado = especialidade2Picker.selectedRow(inComponent: 0)
if estadoIndiceSelecionado >= 0 {
if cidadeIndiceSelecionada >= 0 {
estadoSelecionado = estadosJSON[estadoIndiceSelecionado].nome
cidadeSelecionada = cidades[cidadeIndiceSelecionada].nome
}
}
if especialidade1IndiceSelecionado >= 0 {
if especialidade2IndiceSelecionado >= 0 {
especialidade1Selecionada = especialidadesJSON[especialidade1IndiceSelecionado].nome
especialidade2Selecionada = especialidadesJSON[especialidade1IndiceSelecionado].especialidade2[especialidade2IndiceSelecionado].nome
}
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
if pickerView == estadoClinicaPicker {
return 2
} else if pickerView == especialidade1Picker {
return 1
} else if pickerView == especialidade2Picker {
return 1
}
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if pickerView == estadoClinicaPicker {
if component == 0 {
return estadosJSON.count
} else {
return cidades.count
}
} else if pickerView == especialidade1Picker {
return self.especialidadesJSON.count
} else if pickerView == especialidade2Picker {
return especialidades2.count
}
return 1
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
var rowTitle = ""
let pickerLabel = UILabel()
pickerLabel.textColor = UIColor.black
if pickerView == estadoClinicaPicker {
if component == 0 {
rowTitle = estadosJSON[row].nome
} else {
rowTitle = cidades[row].nome
}
} else if pickerView == especialidade1Picker {
rowTitle = especialidadesJSON[row].nome
} else if pickerView == especialidade2Picker {
rowTitle = especialidades2[row].nome
}
pickerLabel.text = rowTitle
pickerLabel.font = UIFont(name: fontName, size: 16.0)
pickerLabel.textAlignment = .center
return pickerLabel
}
func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
if pickerView == estadoClinicaPicker {
if component == 0 {
return 50
} else {
return 300
}
}
return 300
}
#IBAction func cancel(_ sender: Any) {
navigationController?.dismiss(animated: true)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}
func readJsonEstados() {
let url = Bundle.main.url(forResource: "EstadosECidades", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let jsonResult = try JSONDecoder().decode(RootState.self, from: data)
//handles the array of countries on your json file.
self.estadosJSON = jsonResult.estado
self.cidades = self.estadosJSON.first!.cidades
} catch {
let alerta = Alerta(titulo: "Erro ao Carregar", mensagem: "Erro ao carregar Estados e Cidades. Por favor reinicie o app")
self.present(alerta.getAlerta(), animated: true, completion: nil)
}
}
func readJsonEspecialidades() {
let url = Bundle.main.url(forResource: "Especialidades", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let jsonResult = try JSONDecoder().decode(RootEsp.self, from: data)
//handles the array of specialties on your json file.
self.especialidadesJSON = jsonResult.especialidade
self.especialidades2 = self.especialidadesJSON.first!.especialidade2
} catch {
let alerta = Alerta(titulo: "Erro ao Carregar", mensagem: "Erro ao carregar Especialidades. Por favor reinicie o app")
self.present(alerta.getAlerta(), animated: true, completion: nil)
}
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
Thanks
EDIT
Link to the project https://github.com/afernandes0001/UIAlertController.
You just need to click Log in (no validation is done), go to main screen, Map, Click Search and Apply - There is no need to add any data as I have made data static.
import UIKit
class ViewController: UIViewController {
let button = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
button.backgroundColor = .black
button.setTitle("Alert", for: .normal)
button.setTitleColor(.white, for: .normal)
button.addTarget(self, action: #selector(handleAlert), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
button.heightAnchor.constraint(equalToConstant: 50).isActive = true
button.widthAnchor.constraint(equalToConstant: 100).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
// Do any additional setup after loading the view.
}
#objc fileprivate func handleAlert() {
OperationQueue.main.addOperation {
showAlert(titulo: "YourTilte", mensagem: "YourMessage", vc: self)
print("On main thread: \(Thread.current.isMainThread)")
}
}
extension UIViewController {
func showAlert(titulo: String, mensagem: String, vc: UIViewController) {
let alerta = UIAlertController(title: titulo, message: mensagem, preferredStyle: .alert)
let acaoCancelar = UIAlertAction(title: "Ok", style: .cancel, handler: nil)
alerta.addAction(acaoCancelar)
vc.present(alerta, animated: true)
}
}
it work :)
Try passing in the VC to your Alert class and presenting from there.
class Alert {
class func show(titulo: String, mensagem: String, vc: UIViewController) {
let alerta = UIAlertController(title: titulo, message: mensagem, preferredStyle: .alert)
let acaoCancelar = UIAlertAction(title: "Ok", style: .cancel, handler: nil)
alerta.addAction(acaoCancelar)
//if more than one VC is presenting, or the same one is presenting twice, this might at least tell you which one (assuming it has a title) or when (if the same one is presenting twice)
print("\(vc.title) is presenting alert")
vc.present(alerta, animated: true)
}
}
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Alert.show(titulo: "title", mensagem: "this is an alert", vc: self)
}
}
Call with Alert.show(titulo: "", mensagem: "", vc: self)
If Alert.show(arguments) doesn't work, you may have left the class keyword off of the function. If so, you'll need to create an instance first let alert = Alert() and use alert.show(arguments)
Or you could modify the func to use class properties and create an instance of the class then call instance.show(vc: self) since you already have properties for titulo and mensagem
Make it an extension of UIViewControiller like this:
extension UIViewController {
func showAlert(titulo: String, mensagem: String, vc: UIViewController) {
let alerta = UIAlertController(title: titulo, message: mensagem, preferredStyle: .alert)
let acaoCancelar = UIAlertAction(title: "Ok", style: .cancel, handler: nil)
alerta.addAction(acaoCancelar)
//if more than one VC is presenting, or the same one is presenting twice, this might at least tell you which one (assuming it has a title) or when (if the same one is presenting twice)
print("\(vc.title) is presenting alert")
vc.present(alerta, animated: true)
}}
after simply call wherever you want:
showAlert(titulo: "YourTitle", mensagem: "YourMessage", vc: self)