Below is the issue that i am trying to fix. I have a textView whose text is an attributed string with link attribute. On clicking the link, i should go to other screen. So, I am performing that screen navigation on shouldInteractWithURL() delegate method of the textView. Everything works fine except on force touching the textView, the next page is loading twice. That means shouldInteractWithURL() is called twice on force tap. I fixed it by checking the stack of the view controllers and if the last VC is the one that is about to load, I am returning from there. But, I wanted to know if there is any other solution other than this. Below is the code snippet
textView.text = ""
guard var str = myStr else {
return nil
}
let linkAttribute = [NSLinkAttributeName: NSURL(string: "")!]
var attributedStr:NSMutableAttributedString?
if delay {
str += " "
attributedStr = NSMutableAttributedString(string: str)
let ctaStr = kSuccessStr
let ctaAttributedString = NSAttributedString(string: ctaStr, attributes: linkAttribute)
attributedStr!.appendAttributedString(ctaAttributedString)
} else {
let ctaStr = kFailedStr
attributedStr = NSMutableAttributedString(string: ctaStr, attributes: linkAttribute)
}
textView.linkTextAttributes = [NSForegroundColorAttributeName: UIColor.grayColor()]
textView.attributedText = attributedStr
textView.delegate = thisTableViewDelegate
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool
{
if let textLink = textView.text
{
if (textLink.rangeOfString(str1) != nil) ||
(textLink.rangeOfString(str2) != nil)
{
showSignUpForm(self)
}
else
{
showSuccessfulForm()
}
}
return true
}
Use textView:shouldInteractWithURL:inRange:interaction: instead.
Check if interaction != UITextItemInteractionInvokeDefaultAction and return NO.
See https://developer.apple.com/reference/uikit/uitextviewdelegate/1618606-textview?language=objc
I handle only invokeDefaultAction and it works for me:
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if interaction == .invokeDefaultAction {
//do some stuff
}
return false
}
Related
After running a background-context core data task, Xcode displays the following purple runtime warning when the updates are published in a SwiftUI view:
"[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates."
Besides the ContentView.swift code below, I also added container.viewContext.automaticallyMergesChangesFromParent = true to init in the default Persistence.swift code.
How can I publish the background changes on the main thread to fix the warning? (iOS 14, Swift 5)
Edit: I've changed the code below, in response to the first answer, to clarify that I'm looking for a solution that doesn't block the UI when a lot of changes are saved.
struct PersistenceHelper {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) {
self.context = context
}
public func fetchItem() -> [Item] {
do {
let request: NSFetchRequest<Item> = Item.fetchRequest()
var items = try self.context.fetch(request)
if items.isEmpty { // Create items if none exist
for _ in 0 ..< 250_000 {
let item = Item(context: context)
item.timestamp = Date()
item.data = "a"
}
try! context.save()
items = try self.context.fetch(request)
}
return items
} catch { assert(false) }
}
public func updateItemTimestamp(completionHandler: #escaping () -> ()) {
PersistenceController.shared.container.performBackgroundTask({ backgroundContext in
let start = Date(), request: NSFetchRequest<Item> = Item.fetchRequest()
do {
let items = try backgroundContext.fetch(request)
for item in items {
item.timestamp = Date()
item.data = item.data == "a" ? "b" : "a"
}
try backgroundContext.save() // Purple warning appears here
let interval = Double(Date().timeIntervalSince(start) * 1000) // Artificial two-second delay so cover view has time to appear
if interval < 2000 { sleep(UInt32((2000 - interval) / 1000)) }
completionHandler()
} catch { assert(false) }
})
}
}
// A cover view with an animation that shouldn't be blocked when saving the background context changes
struct CoverView: View {
#State private var toggle = true
var body: some View {
Circle()
.offset(x: toggle ? -15 : 15, y: 0)
.frame(width: 10, height: 10)
.animation(Animation.easeInOut(duration: 0.25).repeatForever(autoreverses: true))
.onAppear { toggle.toggle() }
}
}
struct ContentView: View {
#State private var items: [Item] = []
#State private var showingCoverView = false
#State private var refresh = UUID()
let persistence = PersistenceHelper()
let formatter = DateFormatter()
var didSave = NotificationCenter.default
.publisher(for: .NSManagedObjectContextDidSave)
// .receive(on: DispatchQuene.main) // Doesn't help
var body: some View {
ScrollView {
LazyVStack {
Button("Update Timestamp") {
showingCoverView = true
persistence.updateItemTimestamp(completionHandler: { showingCoverView = false })
}
ForEach(items, id: \.self) { item in
Text(formatter.string(from: item.timestamp!) + " " + (item.data ?? ""))
}
}
}
.id(refresh)
.onAppear {
formatter.dateFormat = "HH:mm:ss"
items = persistence.fetchItem()
}
.onReceive(didSave) { _ in
items = persistence.fetchItem()
}
.fullScreenCover(isPresented: $showingCoverView) {
CoverView().onDisappear { refresh = UUID() }
}
}
}
Since you are performing a background task, you are on a background thread - rather than the main thread.
To switch to the main thread, change the line producing the runtime warning to the following:
DispatchQueue.main.async {
try backgroundContext.save()
}
You should use Combine and observe changes to your background context and update State values for your UI to react.
#State private var coreDataAttribute = ""
var body: some View {
Text(coreDataAttribute)
.onReceive(
CoreDataManager.shared.moc.publisher(for: \.hasChanges)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.global())
.map{_ in CoreDataManager.shared.fetchCoreDataValue()}
.filter{$0 != coreDataAttribute}
.receive(on: DispatchQueue.main))
{ value in
coreDataAttribute = value
}
}
I am working on save image to core data. I used image pick to select image,
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
let currentDateTime = Date()
if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
self.imagePickedBlock?(image,currentDateTime)
} else {
print("Something went wrong")
}
viewController?.dismiss(animated: true, completion:{
if let addPhotoViewController = self.completionViewController as? AddPhotoViewController {
guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else {return }
addPhotoViewController.photoViewModel.image = image
self.viewController?.present(addPhotoViewController, animated: true, completion: nil)
}
})
}
and in addPhotoViewController, I have an confirme button to call the following update database method.
The Photo is an NSManagedObject.
private func updateDatabase(with photoViewModels: [PhotoViewModel]) {
container?.performBackgroundTask { [weak self] context in
for photoViewModel in (self?.photoViewModels)! {
_ = try? Photo.findOrCreatePhoto(matching: photoViewModel, in: context)
}
try? context.save()
self?.printDatabaseStatistics()
}
}
And this is the create NSManagedObject method.
static func findOrCreatePhoto(matching photoViewModel: PhotoViewModel, in context: NSManagedObjectContext) throws -> Photo {
let request : NSFetchRequest<Photo> = Photo.fetchRequest()
// request.predicate if Needed
do {
let matches = try context.fetch(request)
if matches.count > 0 {
return matches[0]
}
} catch {
throw error
}
let photo = Photo(context:context) // the crash line
photo.image = UIImageJPEGRepresentation(photoViewModel.image!, 1)
photo.uploadDate = photoViewModel.createDate
photo.text = photoViewModel.description
// photo.group = try? Group.findOrCreateGroup(matching: photoViewModel, in: context)
return photo
}.
It marks "Enqueued from com.apple.main-thread", I don't really understand where is the problem exactly about the thread, anyone has idea? Don't hesitate if I didn't explain clear enough :)
Thank you for your time.
I found the following issue in my memory. I couldn't understand it.
error evaluating expression “(CAListenerProxy::DeviceAggregateNotification *)0x7cee3d60”: error: use of undeclared identifier 'CAListenerProxy'
error: expected expression
I used two notification centers - one for sending the object to another view and the other in another view to send the dictionary which contain objects after deleting one from it.
My code is :
// only for delegate method for the downloading videos
extension WebViewController {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// her i need to get the data from the movie which i download it so i can save it in the document directory
if let fetchDataFromDownloadFile = try? Data(contentsOf: location) {
// generate the fileName randamlly
let createFileName = UUID().uuidString
// generate object for save file
let operationDocumentDirectory = OperationDocumentDirectory()
operationDocumentDirectory.saveMovie(movieName: createFileName, data: fetchDataFromDownloadFile)
}// end the if let for the fetch data from download file
// her for fetch the download video object when i save it to set it to ni for free the memory
if let fetchURL = downloadTask.originalRequest?.url {
var fetchObject = operationObject?.dictionaryOfDownloadVideo?.removeValue(forKey: fetchURL)
// for stop the downloadtask when finish download
if fetchObject?.videoURL == downloadTask.originalRequest?.url {
downloadTask.cancel()
fetchObject = nil
}
// for update the badge after finish download movie
DispatchQueue.main.async {[weak self] in
if let mySelf = self {
// set badge for nil if the objects zero
if operationObject.dictionaryOfDownloadVideo?.count == 0 {
self?.tabBarController?.viewControllers?[1].tabBarItem.badgeValue = nil
}else{
// if the object not zero update the badge
mySelf.tabBarController?.viewControllers?[1].tabBarItem.badgeValue = "\(operationObject.dictionaryOfDownloadVideo!.count)"
}
}// end the if for myself
}// end the dispatchqueue.main
// update the data in table view
NotificationCenter.default.post(name: NOTIFICATION_UPDATE_TABLEVIEW, object: nil)
}// end the fetch url
}
// and this code in another view for updating table view when i currently downloading movie
extension MovieDownloadingViewController {
// data source
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? DownloadingTableViewCell
if let cell = cell {
cell.movieObject = arrayOfObjects?[indexPath.row]
cell.movieDeleteButton.tag = indexPath.row
cell.movieDeleteButton.addTarget(self, action: #selector(self.deleteCurrentDownloadingMovie(sender:)), for: .touchUpInside)
}
return cell!
}
func deleteCurrentDownloadingMovie(sender:UIButton){
displayAlertDeleteMovie(arrayOfObject: arrayOfObjects!, index: sender.tag)
}
func displayAlertDeleteMovie(arrayOfObject:[DownloadVideo],index:Int) {
let alertController = UIAlertController(title: "Delete Movie", message: "Do You Want Delete Movie \(arrayOfObject[index].videoName)", preferredStyle: .actionSheet)
let alertDelete = UIAlertAction(title: "Delete", style: .default) {[weak self] (alertAction:UIAlertAction) in
var fetchObjectMovie = self?.arrayOfObjects?.remove(at: index)
// set the notification for update the number of element in dict and array
NotificationCenter.default.post(name: NOTIFICATION_UPDATE_NUMBER_OF_ARRAY_DICT, object: fetchObjectMovie?.videoURL)
if fetchObjectMovie != nil {
fetchObjectMovie = nil
}
// update table view
// self?.tableView.reloadData()
// update the badge in the tab bar controller
if operationObject.dictionaryOfDownloadVideo?.count == 0 {
self?.tabBarController?.viewControllers?[1].tabBarItem.badgeValue = nil
}else{
self?.tabBarController?.viewControllers?[1].tabBarItem.badgeValue = "\(operationObject.dictionaryOfDownloadVideo!.count)"
}
}
let alertCancel = UIAlertAction(title: "Cancel", style: .cancel) { [weak self](alertAction:UIAlertAction) in
self?.dismiss(animated: true, completion: {})
}
alertController.addAction(alertDelete)
alertController.addAction(alertCancel)
present(alertController, animated: true, completion: nil)
}
please any help
thanks a lot
I added the new toolbar search in my app, which is awesome. I have some questions regarding it.
How can I remove the cancel icon (x) in the right of the search toolbar?
Also, since the transition is not very smooth between the app toolbar and search toolbar, how can I set the animatelayout etc when I close or go back to the search toolbar?
Currently neither one of those can be customized but it should be pretty easy to do your own custom search support without those. This is discussed in the Toolbar javadoc:
Toolbar.setGlobalToolbar(true);
Style s = UIManager.getInstance().getComponentStyle("Title");
Form hi = new Form("Toolbar", new BoxLayout(BoxLayout.Y_AXIS));
TextField searchField = new TextField("", "Toolbar Search"); // <1>
searchField.getHintLabel().setUIID("Title");
searchField.setUIID("Title");
searchField.getAllStyles().setAlignment(Component.LEFT);
hi.getToolbar().setTitleComponent(searchField);
FontImage searchIcon = FontImage.createMaterial(FontImage.MATERIAL_SEARCH, s);
searchField.addDataChangeListener((i1, i2) -> { // <2>
String t = searchField.getText();
if(t.length() < 1) {
for(Component cmp : hi.getContentPane()) {
cmp.setHidden(false);
cmp.setVisible(true);
}
} else {
t = t.toLowerCase();
for(Component cmp : hi.getContentPane()) {
String val = null;
if(cmp instanceof Label) {
val = ((Label)cmp).getText();
} else {
if(cmp instanceof TextArea) {
val = ((TextArea)cmp).getText();
} else {
val = (String)cmp.getPropertyValue("text");
}
}
boolean show = val != null && val.toLowerCase().indexOf(t) > -1;
cmp.setHidden(!show); // <3>
cmp.setVisible(show);
}
}
hi.getContentPane().animateLayout(250);
});
hi.getToolbar().addCommandToRightBar("", searchIcon, (e) -> {
searchField.startEditingAsync(); // <4>
});
hi.add("A Game of Thrones").
add("A Clash Of Kings").
add("A Storm Of Swords").
add("A Feast For Crows").
add("A Dance With Dragons").
add("The Winds of Winter").
add("A Dream of Spring");
hi.show();
I have the following in a Section:
_favElement = new StyledStringElement (string.Empty);
_favElement.Alignment = UITextAlignment.Center;
if (_room.IsFavourite) {
_favElement.Image = UIImage.FromBundle ("Images/thumbs_up.png");
_favElement.Caption = "Unmark as Favourite";
} else {
_favElement.Image = null;
_favElement.Caption = "Mark as Favourite";
}
_favElement.Tapped += favElement_Tapped;
Then when I press the element I want the following to happen:
private void favElement_Tapped ()
{
if (_room.IsFavourite) {
_favElement.Image = null;
_favElement.Caption = "Mark as Favourite";
} else {
_favElement.Image = UIImage.FromBundle ("Images/thumbs_up.png");
_favElement.Caption = "Unmark as Favourite";
}
_room.IsFavourite = !_room.IsFavourite;
}
However the image and text does not change in the actual element when the element is tapped. Is there a refresh method or something that must be called? I've also tried changing the Accessory on Tapped as well and nothing changes. The properties behind do reflect the correct values though.
An alternative to reloading the UITableView is to reload the Element using code like this (copied from Touch.Unit):
if (GetContainerTableView () != null) {
var root = GetImmediateRootElement ();
root.Reload (this, UITableViewRowAnimation.Fade);
}
assuming that your code is in DialogViewController,add this
this.ReloadData();
but in your case I recommend you to use BooleanImageElement