I have created a custom UIActivity and it was working, and I presented that UIActivityController as per typical tutorials (e.g. here). When configuring that controller I disabled basically all the services:
let items = [location]
let googlemaps = GoogleMapsActivity()
let applemaps = AppleMapsActivity()
let ac = UIActivityViewController(activityItems: items, applicationActivities: [googlemaps, applemaps])
ac.excludedActivityTypes = [.addToReadingList, .assignToContact, .markupAsPDF, .openInIBooks, .postToFacebook, .postToFlickr, .postToVimeo, .postToWeibo, .postToTwitter, .postToTencentWeibo, .print, .saveToCameraRoll]
present(ac, animated: true)
Then I might have stumbled onto an iOS Bug?
When that activity view controller was visible I tapped on "More", which gave me a list of my 2 custom activities. There was no UISwitch to turn them on an off, but there was a typical "row handle" as in a UITableView. I was testing, and tried re-arranging rows. This made one of the activities disappear from that list and now that activity is gone forever.
It won't appear in a list again, even if I delete and re-install the app. It seems I permanently removed the ability for this iOS device to make use of that UIActivity.
What have I done wrong or how can I fix it?
It might have had something to do with these 2 custom activities ultimately sharing the same UIActivity.ActivityType rawValue. I refactored that so they have different values and the issue doesn't seem to occur anymore, or I haven't been able to reproduce it.
Related
I was implementing new feature with share button on my app. Notice the activityViewController appeared blank. at first i thought the item i gave to share might be null, but when i tried to share a simple string, it still shows up like this, i revisited my old working code, and they are all acting like this. Even something as simple as this:
let activityViewController = UIActivityViewController(activityItems: ["test"], applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
DispatchQueue.main.async {
self.present(activityViewController, animated: true, completion: nil)
}
this is what i got:
Anybody have any idea what is causing this and how to fix this?
EDIT: Tested Using Actual Device, Causing problems, in my released APP, the feedback is that its not showing blank, it is showing with options, just not inter-actable, can't be clicked and can't dismiss it, the app will just "hang" and then the user will have to kill the app to reuse it. After testing, i'm still not sure what is causing the blank, i've disabled all my UIViewController extension,(many of them is not automated and need functions to call anyway so i don't think they are the problem), and when i put a debugPrint in the completion block of the present function, it doesn't even get called so the activity view controller is not finished initializing?
Okay, finally found the culprit, somebody tipped me of that i should look into extension of UIViewController to check if i'm overriding something, well i didn't but i did override UILabel and UIButton's awakeFromNib and setNeedsDisplay because my app have multiple language support and In-App change language support and i wanted to "automate" UILabel and UIButton to change language font(because some font is better looking) so that i can avoid attaching listener to viewcontrollers to change language font when they do in-app language changes.
override open func awakeFromNib() {
super.awakeFromNib()
if let prefLang = UserDefaults.languageCode{
self.font = switchFontForLang(lang: prefLang)
}
}
override open func setNeedsDisplay() {
super.setNeedsDisplay()
if let prefLang = UserDefaults.languageCode{
self.font = switchFontForLang(lang: prefLang)
}
}
Particularly the setNeedsDisplay() is causing problem, i've put a debugmessage in them both and i found out its being called endlessly, my guess is because the "in-app" language font changing is trying to change language to the setting but the UIActivityViewController is somehow trying to change the font back or at the very least calling setNeedsDisplay() when it detects something is not right, which it will call into the overrided method and then it will detect it back again thus creating a loop of endlessly calling setNeedsDisplay().
The docs say that subclassing UIAlertController is bad
The UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified.
So what is the recommended way to have an alert that shows not only a title, message and some buttons but also other stuff like ProgressBars, Lists, etc.?
In my special case I would like to have two different alerts, one that shows a ProgressBar and one that shows a list of error messages.
Currently I am trying to add the ProgressView manually and set constraints:
func getProgressAlert(onAbort: #escaping () -> ()) -> UIAlertController {
let alert = UIAlertController(title: "Test", message: "Test", preferredStyle: .alert)
let abort = UIAlertAction (title: "Abort", style: UIAlertActionStyle.cancel) { _ in
onAbort()
}
alert.addAction(abort)
let margin:CGFloat = 8.0
let rect = CGRect(x:margin, y:72.0, width: alert.view.frame.width - margin * 2.0 , height:2.0)
self.progressView = UIProgressView(frame: rect)
self.progressView!.setProgress(0.0, animated: false)
self.progressView!.tintColor = UIColor.blue
alert.view.addSubview(self.progressView!)
self.progressView!.translatesAutoresizingMaskIntoConstraints = false
self.progressView!.widthAnchor.constraint(equalTo: alert.view.widthAnchor, multiplier: 1.0).isActive = true
self.progressView!.heightAnchor.constraint(equalToConstant: 5.0).isActive = true
self.progressView!.topAnchor.constraint(equalTo: alert.view.topAnchor).isActive = true
self.progressView!.leftAnchor.constraint(equalTo: alert.view.leftAnchor).isActive = true
return alert
}
I don't think this is the way this should be done as manually defining constraints is very prone to errors on different devices. For example, the current code just shows the progress bar on the top of the alert view, but I want it to be shown between the message and the abort button.
The view hierarchy for this class is private and must not be modified.
This is pretty much the nail in the coffin for this API. If you try hack it, you will cause yourself a lot of pain trying to support it across different iOS versions.
If want to have custom controls on an alert you will have to write a custom UIViewController subclass and mimic the API as best as you can whilst adding your new functionality (if you do not want to do this, there will be examples available on GitHub).
Some examples:
https://github.com/Codeido/PMAlertController
https://github.com/Orderella/PopupDialog
https://github.com/vikmeup/SCLAlertView-Swift
https://github.com/sberrevoets/SDCAlertView
The docs do indeed discourage subclassing UIAlertController. However it's easy to find examples of people circumventing this and sneaking subviews onto the alert's view, but they do so at their own peril of a minor iOS version update breaking it.
A big reason Apple places such limitations is because they reserve the right to change how this class works behind the scenes. UIAlertController does a lot of heavy lifting, and it used by many apps including those shipped by Apple.
But it only does what it does, and doesn't do what it doesn't. I don't think it's an accident that UIAlertController does not support the modal progress indication use case you describe. These types of UI "roadblocks" conflict with a good user experience.
Is there another way to achieve the same goals without using a modal? This might involve disabling other aspects of your UI until the work is done, but still allowing the user to navigate. This would yield a better UX.
But if that's not going to work for you, and you must use a roadblocking modal progress indicator, acknowledging the negative UX it creates, it's more prudent and reliable to just build your own. UIAlertController is nothing special, it's a UIViewController like any other. It uses publicly available API to control its frame size, how it overlays its presenting view, and how it animates on and off the screen. You'll save yourself a lot of headache by just rolling your own.
I am using iOS UITest for a Swift application. I use something like,
func testAllScreenNavigation() {
let app = XCUIApplication()
app.tabBars.buttons["Home"].tap()
app.navigationBars["Home"].buttons["More"].tap()
app.sheets.buttons["Cancel"].tap()
}
etc. to navigate some of the specific, tabs, buttons, etc. and switch to respective screens. But i want to navigate each and every screens of my Application (It can be BFS style navigation or DFS style navigation, no matter). Is there any way iOS provides so i can get all navigable elements and then explore deeper and deeper automatically for my App?
I also need to keep trace of which xcuoelement in a screen is already processed and which are not yet processed.
The only way I can think of is using Xcode UI test recorder feature.
While you are recording, navigate through all of your screens via the device/simulator and then the XCUIApplication() variable would be recorded with the appropriate references.
If the button/nav bar/any element has text on it, it will show up in the recorded code or else it will be referenced numerically.
Hope that helps.
Kind regards,
Mukund
I like your idea for getting all views and check whether the layouting and localization for example is fine.
I think you need to specify your criteria for "screens" and how they are accessed.
Basically, one could thing of the following structure
- UITabBarController
-- UISplitViewController
--- UINavigationController
---- UIViewController
----- UIBarButtonItems
----- UIView
----- UIButton
----- UISwitch
----- UITableViewCell
You could now go top down from the UITabBarController to the next controlling instance (might also skip one, e.g. SplitViewControllers on iPhones).
You can use the general property:
XCUIApplication().tabBars
Nevertheless that transition is the problem: How would you get from one ViewController to another and are they all position in the ViewController's View or do you have to loop the subviews of a view.
UIButton -> Touch Up Inside
UISwitch -> Value Changed
UITableViewCell -> DidSelectRowAtIndexPath
UIView -> UILongPressGestureRecognizer
This is how I would basically set it up:
For each UIViewController instance, get the related View (and perform the following call recursively).
Check all the subviews of a view.
For UIViews, go even further and check their subviews
For UIButtons, perform TouchUpInside
and so on.
Make sure to have a condition to stop going deeper, as UITableViews got a lot of subviews or your UIWebViews would of course be set up in a different way.
This way you should be able to navigate through a lot Views in your app hierarchy, but you will need some extensions for UIBarButtonItems, custom Gesture Recognizers and of course also for your "special" controls that might listen to value changes and perform a layout-change.
Accessing specific elements
In addition to the above approach where you simply get an array of elements of a specific type, you can access specific elements (e.g. those where you know they are of a very specific type with certain ValueChangeListeners or something)
To access a specific object in particular, like the TabBar example from above, you can use the accessibilityLabel like so. At first you need to declare the accessibilityLabel in your code or in the .xib-file/.storyboard:
// just to illustrate, so you get an idea:
self.tabBarController.isAccessibilityElement = true
self.tabBarController.accessibilityLabel = "tabBar"
And then do:
let tabBar = XCUIApplication().tabBars["tabBar"]
Here is Apple's documentation for setting these accessibilityLabels:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/iPhoneAccessibility/Making_Application_Accessible/Making_Application_Accessible.html
A great way to get the related identifier of an element would be to use the Accessibility Inspector from Apple:
https://developer.apple.com/library/content/technotes/TestingAccessibilityOfiOSApps/TestAccessibilityiniOSSimulatorwithAccessibilityInspector/TestAccessibilityiniOSSimulatorwithAccessibilityInspector.html
Accessing elements in general
To access elements in general, you need to make use of the XCUIElementType of these objects, here you will access the objects based on their classes.
E.g. you could call:
"tabBars", "navBars", "tables", "buttons", and so on from the elements in general.
Still you would be facing the issue with "special controls". As the Apple documentation lacks (imho) some detail about properties and attributes, I do recommend the docs here: https://blog.metova.com/guide-xcode-ui-test/ It provides a great overview of what is accessible and may help you getting some better understanding.
An overview of the available XCUIElementTypes can be found here. Basically, the elementType property is an enumerated value that represents the type of an element. XCUIElementType is a very large enumeration and some of its members do not apply to iOS applications (they apply to MacOS X apps). Some of the more commonly used values are:
Alert
Button
NavigationBar
TabBar
ToolBar
ActivityIndicator
SegmentedControl
Picker
Image
StaticText
TextField
DatePicker
TextView
WebView
https://developer.apple.com/reference/xctest/xcuielementtype?language=objc
I need advice in situation: I have several JSON where may be several types of transport are (for example, only train (first variant) or train and bus (second variant). I know that there are only 3 types of transport maximum.
So, I'd like to show info from JSON about train in first view controller, info from JSON about bus in second end etc.
How better to do: create several view controllers (for maximum variants - 3), several tabBar.items (3) and when I get data from JSON in AppDelegate I will know: "OK, I know that in that JSON info only about train and I should show only tabBar.item = "train" and work only with TrainViewController and others tabBar.items I must hide from user? Is it good experience?
Your question will have multiple solutions to achieve your goal and totally depends on what kind of UI will attract your users. But along with the UI I will also advice you to consider the app size and code complexity.
If I would have to do this, I would have done like this:
1) Use single `ViewControlller` with `SegementedControl` on top having titles of your variants.
2) Whenever user selects the `segment`,load necessary data for that variant.
3) If you are going to show that data in list format, then It would be very easy to manage your `datasource` as you can simply replace the datasource depending on the selected variant.
This is not the exact or the only solution, but IMO this will reduce app size as you will be using single ViewController instead of three and you can easily manage all your complexity in a single ViewController class.
I would go about your tab bar programmatically. I would create a new Cocoa Touch Class, calling it something like CustomTabBarController and subclassing it as a UITabBarController. Go ahead to your App Delegate file and inside of your didFinishLaunchingWithOptions function, add the following:
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = CustomTabBarController()
Now, when your app launches, your rootViewController will be this tab bar view. Now inside of your viewDidLoad in your CustomTabBarController class, you'd simple implement your viewControllers in an array, which your tab bar will show and go to when touched like so:
let trainController = UIViewController()
let trainNavigationController = UINavigationController(rootViewController: trainController)
trainNavigationController.tabBarItem.image = UIImage(named: "your_tab_icon")?.withRenderingMode(.alwaysOriginal)
trainNavigationController.tabBarItem.selectedImage = UIImage(named: "your_tab_selected_icon")?.withRenderingMode(.alwaysOriginal)
let busController = UIViewController()
let busNavigationController = UINavigationController(rootViewController: trainController)
busNavigationController.tabBarItem.image = UIImage(named: "your_tab_icon")?.withRenderingMode(.alwaysOriginal)
busNavigationController.tabBarItem.selectedImage = UIImage(named: "your_tab_selected_icon")?.withRenderingMode(.alwaysOriginal)
viewControllers = [trainNavigationController, busNavigationController]
As for the JSON part, that's a totally different ball game. There are many tutorials online and on SO. Hope this helps and points you in the right direction. Good luck!
My UIActivityViewController presents an image with some text underneath it:
let objectsToShare: [AnyObject] = [anImage, someText]
let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
It's adding an undesired line break in between the image and the text when shared (red highlight):
Is there a way to remove it? FYI someText does not have any hard-coded line breaks in it. I'm impartial to an Objective-C or Swift answer to this.
Since you are not really in control of rendering the activities, only supplying them, you don't have control over the padding that is placed between the content and the activities.
I would explain to my designer/client/boss that it's out of your control, unfortunately, and file a rdar if you really think this is something you should have control of (I don't think it is, personally).
Hacky and not at all recommended...
Since UIActivityViewController is a UIViewController, you could, I guess, subclass it and iterate through the subviews in the .view of that object to find the right view/constraint and adjust it yourself but this is truly not something I would go near even if my job depended on it. It assumes so much about how Apple implemented that views hierarchy that you are a hostage to fortune that it will fail in any future iOS update where Apple may decided to rework that class.