UIActivityViewController "Save to Files" saves multiple files when only 1 file is required - swift

My app is able to provide a variety of UIActivityItem types (text, data, rich text, print page renderer, etc) for sharing for a variety of purposes, including printing, copy/paste, and saving as a file. For the purpose of copy/paste, it needs to include plain text, attributed string, data (JSON Data) and JSON string).
However, because several data types are provided, the UIActivityViewController's "Save to Files" option results in several files being saved - one for each of the item types that can be saved as a file.
If I reduce it to just one UIActivityItem, then copy/paste functionality is severely reduced so that it would not work with all of the various different pasteboard types it should (eg, my app's custom JSON data format AND with plain text AND attributed string).
So I'm attempting to use a UIActivityItemProvider subclass to overcome the issue, but I still cannot figure out how to get only one file to be saved, instead of multiple files (one for each item type). Is this even possible?
Relevant parts of my UIActivityItemProvider subclass are below.
(Note that I'm using multiple instances of a single subclass for this, but the same problem would occur with using different subclasses.)
class RatsActivityItemProvider: UIActivityItemProvider {
var rats: [Rat]
init(placeholderItem: Any, rats: [Rat]) {
RatsActivityItemProvider.selectedOption = nil
self.rats = rats
super.init(placeholderItem: placeholderItem)
}
class func allProviders(forRats rats: [Rat]) -> [RatsActivityItemProvider] {
var providers: [RatsActivityItemProvider] = []
providers.append(RatsActivityItemProvider(placeholderItem: NSAttributedString(), rats: rats))
providers.append(RatsActivityItemProvider(placeholderItem: RatPrintPageRenderer(), rats: rats))
providers.append(RatsActivityItemProvider(placeholderItem: [:] as [String:Any], rats: rats))
providers.append(RatsActivityItemProvider(placeholderItem: Data(), rats: rats))
return providers
}
override var item: Any {
print("\(activityType!.rawValue as Any) - \(type(of: placeholderItem!))")
switch activityType {
case UIActivity.ActivityType.print:
return RatPrintPageRenderer(rats)
case UIActivity.ActivityType.copyToPasteboard:
var pasteboardDict: [String:Any] = attrString.pasteables()
// (Add custom types to dictionary here)
return pasteboardDict
default:
// "Save To Files" activity is not available in UIActivity.ActivityType so check the raw value instead
if activityType?.rawValue.contains("com.apple.CloudDocsUI.AddToiCloudDrive") ?? false {
//
// HOW TO HAVE ONLY ONE OF THE PROVIDERS RETURN A VALUE HERE???
//
}
}
}
}
When I run this and choose "Save to Files" I get the following output (one line from each of the providers):
com.apple.CloudDocsUI.AddToiCloudDrive - NSConcreteAttributedString
com.apple.CloudDocsUI.AddToiCloudDrive - RecipePrintPageRenderer
com.apple.CloudDocsUI.AddToiCloudDrive - __EmptyDictionarySingleton
com.apple.CloudDocsUI.AddToiCloudDrive - _NSZeroData
...and a file gets created for each of these, if I simply pass back the item for that data type.

Well, I've found that the solution was two-fold...
Direct Answer To Question (but not ideal behaviour):
In each of the switch cases (and the if within the default case), I needed to check that the activityType matches the placeholderItem. If it is not a suitable match, then return the (empty) placeholderItem as-is (except that in the "Save to Files" case, even the empty print page renderer placeholder resulted in a file being written! so instead of returning the placeholder there, return an empty array).
This worked, and resulted in a single file being written to a location of the users choice, which answers the original question. The user even gets the chance to provide a name for the file. But the default name is not good at all - just the data type (eg, "text" or "data", depending on what was being saved to the file).
Solution that Gives More Flexibility:
Create a custom UIActivity that will write the file to a location chosen by the user using a UIDocumentPickerViewController. Eg, The activity can have a title like, "Export to ").
This proved to be a lot more flexible, and let me use a default file name (and extension!) that made a lot more sense, based on the data being passed in. It also has the potential for me to add further improvements to behaviour later (eg, I might be able to use an alert to get the user to choose between a couple of different file formats (to use for different purposes).
This does not replace the "Save to Files" action, so I end up with both of them, and still needed to fix my "Save to Files" action's behaviour as described above.
It leaves me with both an "Export" and a "Save" action, which work similarly, but one is more flexible and intuitive than the other, and I can make them use different file formats by default.
My new code (for the default part of the outer switch) is below...
default:
if activityType?.rawValue == "com.stuff.thing.activity.export" {
if placeholderItem is [Rat] {
return rats
} else {
return placeholderItem!
}
} else if activityType?.rawValue == "com.apple.CloudDocsUI.AddToiCloudDrive" {
if placeholderItem is Data {
return attrString
} else {
// Don't return the placeholder item here! Some (eg print page renderer) can result in a file being written!
return [] as [Any]
}
}
return attrString
}

Related

Returning variables from Model to other ViewControllers

I am making a weather application. I basically created a class where I will only get data from API and return them as needed. I have variables like cityName, currentWeather etc.
Problem is sometimes API doesn't provide them all so I need to check if they are nil or not before return them. What comes to my mind is set variables like this first:
private var cityNamePrivate: String!
and then
var cityNamePublic: String {
if cityNamePrivate == nil {
//
}
else { return cityNamePrivate }
But as you can as imagine, its very bad because I have a lots of variables. Is there any better logic for this? Should I just return them and check later in wherever I send them?
The problem here is that you will have many variables to deal with. It's not just returning them from the API, it's also dealing with them in your app and perhaps in some processing code.
One solution is to have a big class or struct with many properties. This will work very well, is straightforward to implement but will require lots of repetitive code. Moreover, it will require to change your API and all your code, whenever some new properties are made available by the remote web service.
Another approach is to have replace the big inflexible class or struct, with a dynamic container, e.g. an array of items [ Item ] or a dictionary that associates names to items [ String : Item ]. If the data is just strings, it's straightforward. If it's several types, you may have to have to implement a small type system for the elements. Example:
var x : [ String: String] = [:]
x["city"]="Strasbourg"
x["temperature"]="34°C"
struct Item {
var name : String
var value : String
}
var y : [ Item ] = [Item(name:"city",value:"Strasbourg"),
Item(name:"temperature", value:"34°C")]
The other advantage of this approach is that you stay loyal to the semantics: an information that is not available in the API (e.g. "unknown") is not the same as a default value. I.e. if the weather API does not return a temperature, you will not display 0 in your app. because 0 is not the same as "unknown". While strings are more robust in this matter, the absence of a name is not the same as an empty name.
This last remark suggests that in your current scheme, of having a big data transfer object, you should consider to keep the properties as optional, and move the responsibility for the processing of unknown data to your app.

VSCode Extension - Get Editor Types

I'm trying to determine if the active editor is opened by git or not (i.e. files that typically end in Working Tree, Untracked, etc)
There is little difference in vscode.window.activeTextEditor from the original file vs the same file opened from git with the modifications view, save for one field:
(vscode.window.activeTextEditor as any).id
Which returns a string like this:
'vs.editor.ICodeEditor:1,$model12' // value for normal editor
'vs.editor.ICodeEditor:7,$model12' // value for git editor
The value 7 returned for ICodeEditor will be consistent across all editors within a particular vscode instance, but changes depending on what editors are available.
So I'm trying to translate the magic number 7 (in this one particular case) to the EditorType
In the source code, there's an editorBrowser which has the following function:
export function isDiffEditor(thing: any): thing is IDiffEditor {
if (thing && typeof (<IDiffEditor>thing).getEditorType === 'function') {
return (<IDiffEditor>thing).getEditorType() === editorCommon.EditorType.IDiffEditor;
} else {
return false;
}
}
And editorCommon has an EditorType:
export const EditorType = {
ICodeEditor: 'vs.editor.ICodeEditor',
IDiffEditor: 'vs.editor.IDiffEditor'
};
But I cannot figure out how to access any of that information from within the context of an extension.
Even window.activeTextEditor which returns type TextEditor doesn't explicitly offer up fields like id (which is why I had to cast to any above). So some of the internals here might not show up in the docs / public facing interfaces.

How can customData can be binded with JavaScript

In the affected application is a responsive table whose ColumnListItems are added via JavaScript code. Now the lines should be highlighted by the highlighting mechanism depending on their state. The first idea was to control the whole thing via a normal controller function. I quickly discarded the idea, since the formatter is intended for such cases. So I created the appropriate Formatter function and referenced it in the JavaScript code. The call seems to work without errors, because the "console.log" is triggered in each case. Also the transfer of fixed values is possible without problems. However, the values I would have to transfer are located within customData of each line...
No matter how I try to form the path I get an "undefined" or "null" output.
I have already tried the following paths:
"/edited"
"/customData/edited"
"mAggregations/customData/0/mProperties/value"
"/mAggregations/items/0/mAggregations/customData/0/mProperties/value"
The code from Controller.js (with consciously differently indicated paths):
var colListItem = new sap.m.ColumnListItem({
highlight: {
parts: [{
path: "/mAggregations/items/0/mAggregations/customData/0/mProperties/value"
}, {
path: "/edited"
}],
formatter: Formatter.setIndication
},
cells: [oItems]
});
// first parameter to pass while runtime to the formatter
colListItem.data("editable", false);
// second paramter for the formatter function
colListItem.data("edited", false);
oTable.addItem(colListItem);
The code from Formatter.js:
setIndication: function (bEditable, bEdited) {
var sReturn;
if (bEditable && bEdited) {
// list item is in edit mode and edited
sReturn = "Error";
} else if (bEditable || bEdited) {
// list item is in edit mode or edited
sReturn = "Success";
} else {
sReturn = "None";
}
return sReturn;
}
The goal would also be for the formatter to automatically use the value of the model in order to avoid its own implementation of a listener, etc.
I hope one of you has a good/new idea that might bring me a solution :)
Many thanks in advance!
You cannot bind against the customData. Because the customData is located in the element, it is like a property.
Thats why you defined it here on colListItem: colListItem.data("key", value)
You only can bind against a model.
So I see three solutions
Store the information in a separate local JSON model whereof you can speficy your binding path to supply the values to your formatter
Do not supply the information via a binding path to the formatter, but read a model/object/array from a global variable in the controller holding the information via this (=controller) in formatter function
Store the information in the customData of each element and access the element reference in the formatter function via this(=ColumnListItem).data().
Passing the context to the formatter similar to this formatter: [Formatter.setIndication, colListItem]
Cons of 1. and 2: you need a key for a respective lookup in the other model or object.
From what I understand I would solve it with solution 3.

IT Hit WebDAV Ajax Browser Custom Columns

I am looking at a trial of the Ajax Browser Control by ItHit. So far it seems to be pretty responsive when it comes to pulling files across http protocol.
What I want to do at this point is have the details view pull custom properties from my excel workbooks. What is the most efficient way to connect my C# code that gets the custom properties to the Ajax control to display the correct values?
The easiest way to create a custom column is to return custom property from a WebDAV server. In the example below the server returns price in PricesNs:RetailPrice property.
On a client side you will define a custom column and specify custom property name and namespace:
{
Id: 'MyColumn1',
CustomPropertyName: 'RetailPrice',
CustomPropertyNamespace: 'PricesNs',
Text: 'Retail Price',
Width: '150px'
}
Another approach is to return an HTML from a Formatter function specified for column. You have a full control over what is being displayed in this case.
You can find more details and an example in this article: http://www.webdavsystem.com/ajaxfilebrowser/programming/grids_customization/
In case your WebDAV server is running on IT Hit WebDAV Server Engine, to return the requested property, you must implement IHierarchyItem.GetProperties method (or its asynchronous counterpart):
public IEnumerable<PropertyValue> GetProperties(IList<PropertyName> names, bool allprop)
{
if (allprop)
{
return getPropertyValues();
}
List<PropertyValue> propVals = new List<PropertyValue>();
foreach(PropertyName propName in names)
{
if( (propName.Namespace == "PricesNs") && (propName.Name == "RetailPrice") )
{
// Depending on the item you will return a different price,
// but here for the sake of simplicity we return one price regerdless of the item
propVals.Add(new PropertyValue(propName, "100"));
}
else
{
...
}
}
return propVals;
}

Shared (or static) variable in Swift

I have a class with an array which values comes from a text file. I would like to read this values once and store them in a shared variable, making possible other classes access that values.
How can I do that in Swift?
UPDATE:
Suppose I have three classes of animals and which of them can be found in a set of places which is load from differents tables (each animal have yours and the structure is different for each one). I would like to be able to use them linking to specific class:
clBirds.Places
clDogs.Places
clCats.Places
Note that I need to load data once. If I dont´t have a shared variable and need to put it outside the class, I need to have different names to the methods, just like:
BirdsPlaces
DogsPlaces
CatsPlaces
And we don´t have heritage in this case
Declare the variable at the top level of a file (outside any classes).
NOTE: variables at the top level of a file are initialized lazily! So you can set the default value for your variable to be the result of reading the file, and the file won't actually be read until your code first asks for the variable's value. Cool!
Similarly, you can declare an enum or struct at the top level of a file (outside any classes), and now it is global. Here's an example from my own code:
struct Sizes {
static let Easy = "Easy"
static let Normal = "Normal"
static let Hard = "Hard"
static func sizes () -> String[] {
return [Easy, Normal, Hard]
}
static func boardSize (s:String) -> (Int,Int) {
let d = [
Easy:(12,7),
Normal:(14,8),
Hard:(16,9)
]
return d[s]!
}
}
Now any class can refer to Sizes.Easy or Sizes.boardSize(Sizes.Easy), and so on. Using this technique, I've removed all the "magic numbers" and "magic strings" (such as NSUserDefault keys) from my code. This is much easier than it was in Objective-C, and it is much cleaner because (as the example shows) the struct (or enum) gives you a kind of miniature namespace.