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.
Related
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.
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().
Currently I am working on a project which got all the task done in a singe viewController. As there are so many elements on the viewController I choose to do the UI with coding, like this:
let myButton: UIButton = {
let button = UIButton(type: .system)
button.addTarget(self, action: #selector(openFunction), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
As there are so many button, view, texifield, label etc along with all their constraints written with code, my viewController class getting bigger and bigger. How can I keep all the UI code in separate file and integrate in on my viewController ? I don't know there might be really easy way to do that but I am actually struggling. Any suggestion would be really helpful.
Welcome to the world of design patterns and code architecture. They are various ways to accomplish what you are after. It's a good sign you are able to identify this problem early.
You can start looks at MVVM, VIPER, ReSwift among others. Research which fits your the requirements of your app.
Suggestions for Reducing UI Code in view controller:
In terms of reducing just the UI Code growing in the view controller, I suggest start creating subclasses of common elements and keey your code DRY. For instance, if a UIButton with same fonts and borders etc are being created many times then look at creating a subclass for it and move the configurations inside this subclass.
You can also create subview of logical elements on the screen, example you have a header with buttons and labels then move it into a subclass and start using this subclass from here on. This should improve your code readability and reuse.
You can also reduce a lot of the autlayout code by create extensions of commons layouts like pinning to all corners etc this way the repetition of boilerplate auto layout code is much less.
An alternative to what carbonr has proposed is to leverage Interface Builder. With Interface Builder, you can create one or more StoryBoards and separate UI elements and constraints from the controller that contains your code. Obviously, if you are unfamiliar with Interface Builder there would be a learning curve.
A specific answer to your specific code would be to create a convenience initializer in an extension to UIButton.
extension UIButton {
convenience init(_ target:Any, _ action:Selector) {
self.init(CGRect.zero)
self.addTarget(target, action:action)
self.translatesAutoresizingMaskIntoConstraints = false
}
}
Right there you are probably cutting back on things in your VC.
Next, consider moving this - and all - UI code out of your VC file and into other files. I typically move my extensions/subclasses into as many files as needed. (The build may take longer but the final binary should be the same size.) For large projects, this helps make things manageable.
At the same time consider making an extension to your VC specifically for auto layout (which I see you are using because you are setting your UIButton auto resizing mask). As long as you are declaring your objects in the main subclassed VC, this removes the "verbose" nature of auto layout into it's own file.
For multi-developer projects and/or true "reusable" code, a final thing you can do is move code into a Framework target.
The solution in this question* uses setHidden to hide and unhide a WKInterfaceGroup:
atypeofGroup.setHidden(true)
atypeofGroup.setHidden(false)
But the problem is, the group will appear and disappear abruptly, it doesn't look professional. Can someone guide me please? Not sure whether it is related to this:
atypeofGroup.animationDidStart(anim: CAAnimation!)
*hide and show WKInterfaceGroup programmatically
This is a great question, but it just isn't possible to animate a change between two groups with the current implementation of WatchKit. I definitely wish it was as well.
The only options you have are to switch interface controllers entirely through the reloadRootControllersWithNames:contexts: or to show/hide a couple of groups using the approach you listed first. Here's a small example of how you could switch from a SimpleInterfaceController to a FirstInterfaceController and SecondInterfaceController in a page set.
class SimpleInterfaceController : WKInterfaceController {
override func willActivate() {
super.willActivate()
let names = ["FirstInterfaceIdentifier", "SecondInterfaceIdentifier"]
WKInterfaceController.reloadRootControllersWithNames(names, contexts: nil)
}
}
I am not sure where you found the following code snippet, but it is certainly not part of the public APIs on WKInterfaceGroup.
atypeofGroup.animationDidStart(anim: CAAnimation!)
While I understand none of these answers are ideal, they are all we have access to at the moment. If you have the time, I'd suggest filing a feature request on Apple's bug reporting system.
I am sure the answer to this is "no" as the documentation is very clear. But I am a little confused. A standard UIAlertView is pretty dull and I want to improve the look and it seems that other apps do it (see the example below).
Another possibility is that they are not subclassed UIAlertViews. In which case, how is this achieved?
The page UIAlertViews states
Appearance of Alert Views
You cannot customize the appearance of alert views.
So how do we get the something like the example shown here?
No, do not subclass it. From the docs:
Subclassing Notes
The UIAlertView 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.
What you can do though is create a UIView and have it act similar to a UIAlertView. It's isn't very difficult and seems to be what they are doing in your op.
Apple's docs say that you should not subclass it. That means that there are probably internal reasons that would make it difficult to make it work right.
You might or might not be able to make a subclass of UIAlertView work, but you do so at your own risk, and future iOS releases might break you without warning. If you tried to complain Apple would laugh and tell you "I told you so".
Better to create a view that looks and acts like an alert but is your own custom view/view controller. Beware that even this is dangerous, because Apple has been making sweeping changes to the look and feel of it's UI elements recently. If you implement a view controller that looks and acts like a variant of the current alert view, Apple could change that look and/or behavior in the future and your UI app would end up looking odd and outdated. We've been bitten by this sort of thing before.
Rethink your strategy. Why do you need to use an Alert View? Besides having a modal view displayed top-most on your view stack, there's not much else that it does. Instead, subclass UIView or UIViewController to define your own interface, using images and ui elements to give it the style and input functionality as needed.
I usually subclass UIView, and attach it to the app's window's view so that I'm certain that it will be displayed on top of anything else. And you can use blocks to provide hooks into the various input elements of your new view (did user press OK, or did user enter text?)
For example:
// Instantiate your custom alert
UIView *myCustomAlert = [[UIMyCustomUIViewAlert alloc] initWithFrame:CGRectMake(...)];
// Suppose the new custom alert has a completion block for when user clicks on some button
// Or performs some action...
myCustomAlert.someEventHandler = ^{
// This block should be invoked internally by the custom alert view
// in response to some given user action.
};
// Display your custom alert view
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
[window addSubview: myCustomAlert];
// Make sure that your custom alert view is top-most
[window bringSubviewToFront: myCustomAlert];
Using this method, however, will not pause the thread's execution like UIAlertView does. Using this method, everything will continue running as usual. So if you need to pause execution while your custom alert is showing, then it gets much trickier.
But otherwise, creating your own custom alerts is quite straightforward, just as you would customize any other view. You could even use Interface Builder.
Hope this helps.
No. You absolutely should not subclass a UIAlertView for any reason. Apple explicitly states this in their documentation (see "Subclassing Notes"). They even tell you that it relies on private methods - and we all know that meddling in private methods in an AppStore app is immediate grounds for rejection.
HOWEVER, there isn't a need to subclass UIAlertView on iOS 7. Apple introduced a new Custom ViewController Transitions feature in iOS 7.0 that lets you present completely custom ViewControllers with completely custom transitions. In other words, you could very easily make your own UIAlertView or even something better. There's a nice tutorial on the new feature here:
In fact, there are lots of good tutorials on this - a quick Google search on the topic turns up a huge wealth of information.