102 comments

[ 3.2 ms ] story [ 271 ms ] thread
Just to be clear, UIWebBrowserView is a private class, not the public UIWebView people should use, so it's not like Apple started rejecting random apps for no good reasons...
Thanks for the info.

Is your obj-c good enough to explain what Ionic is actually doing in the file that triggers the problem? https://github.com/driftyco/ionic-plugin-keyboard/blob/maste... Are they really doing it in a 'bad' way?

They are accessing an internal subview of UIWebView to hide the inputAccessoryView from the keyboard.

When you use a normal view that uses the keyboard (UITextView, etc.), you can set it to any custom view you want, but because UIWebView is supposed to be used to display websites and not full apps, that property is not accessible, so they access the internal hierarchy of the webView, find the actual view that triggers the keyboard and modify it's property.

> They are accessing an internal subview

An internal undocumented subview which is part of a private framework, aka they're digging into UIWebView's private implementation details[0] in order to do whatever they want to. Which may or may not be possible without that, but sadly that's besides the point.

It's interesting to see that below the now-offending bits are even hackier bits which are currently commented "until [they] know it's app store safe"

[0] which can't be formally private due to the architecture of Cocoa and objective-c I guess

Just wanted to chime in and say that's actually really bad code. Accessing a views subviews like that and looking for a specific instance of a class is just begging for a crash.
After reading lines 5 and 6 it does not surprising me.
Counterpoint: they have all the nil checks needed for this to fail silently if no UIWebBrowserView is found. It's a fragile implementation in that it might stop working if Apple changes the internal implementation of UIWebView, but it doesn't look like it'll crash because of it.

That said, clearly it's not good for Apple to have large numbers of apps relying on private implementation details. It hamstrings them when it comes time to improve the OS. That translates to a harm for users. I'm not advocating for this hack.

I wonder if this could be avoided by switching to WKWebView?

> I wonder if this could be avoided by switching to WKWebView?

Nope, the keyboard UI is pretty much the same and the API does not provide any more support for disabling the accessory bar.

   - (UIView *)hackishlyFoundBrowserView {
       UIScrollView *scrollView = self.scrollView;
  
       UIView *browserView = nil;
       for (UIView *subview in scrollView.subviews) {
           if ([NSStringFromClass([subview class]) hasPrefix:@"UIWebBrowserView"]) {
               browserView = subview;
               break;
           }
       }
       return browserView;
   }
This iterates through the view hierarchy until it finds a view with class name starting with "UIWebBrowserView".

   - (void)ensureHackishSubclassExistsOfBrowserViewClass:(Class)browserViewClass {
       if (!hackishFixClass) {
           Class newClass = objc_allocateClassPair(browserViewClass, hackishFixClassName, 0);
           IMP nilImp = [self methodForSelector:@selector(methodReturningNil)];
           class_addMethod(newClass, @selector(inputAccessoryView), nilImp, "@@:");
           objc_registerClassPair(newClass);
  
           hackishFixClass = newClass;
       }
   }
This creates a new class at runtime that is a subclass of UIWebBrowserView (not accessible at compile time), but with the getter method for the inputAccessoryView overridden to return nil instead of the accessory view.

   - (void) setHackishlyHidesInputAccessoryView:(BOOL)value {
       UIView *browserView = [self hackishlyFoundBrowserView];
       if (browserView == nil) {
           return;
       }
       [self ensureHackishSubclassExistsOfBrowserViewClass:[browserView class]];
  
       if (value) {
           object_setClass(browserView, hackishFixClass);
       }
       else {
           Class normalClass = objc_getClass("UIWebBrowserView");
           object_setClass(browserView, normalClass);
       }
       [browserView reloadInputViews];
   }
This takes the UIWebBrowserView object and overrides it's class at runtime with the aforementioned subclass with the nil accessory view getter.

(Side note, this could also maybe have been done by swizzling out the method on the internal class instead of subclassing at runtime)

I agree with the swizzling preference.

I wonder if they can go around the Apple issue by using something like https://github.com/UrbanApps/UAObfuscatedString to generate the "UIWebBrowserView" string.

static NSString browserViewClassName = Obfuscate.U.I.W.e.b.B.r.o.w.s.e.r.V.i.e.w;

   - (UIView *)hackishlyFoundBrowserView {
       UIScrollView *scrollView = self.scrollView;
  
       UIView *browserView = nil;
       for (UIView *subview in scrollView.subviews) {
           if ([NSStringFromClass([subview class]) hasPrefix:browserViewClassName]) {
               browserView = subview;
               break;
           }
       }
       return browserView;
   }
This generates the "UIWebBrowserView" string at runtime which may get around Apple's checks.
Probably could try a number of different things to sneak it through, but I would guess that if you attempt to sneak through something that explicitly got you rejected previously, you might run the risk of getting your Bundle banned.
Yeah the view in question is a private[0] sub-sub view of UIWebView (UIWebView > UIScrollView > UIWebBrowserView) which does the actual rendering

[0] there is no direct access to it, though because of Cocoa's architecture you can access it by iterating the scrollview's subviews and look for one with the expected class name

That sounds really screwed up. You can access private methods?
Yes, which is useful because it lets us work around bugs in Apple's own frameworks. (There is a trend with Swift to make everything more safe and enterprisey, as in JVM languages, though.)

But this isn't about calling private methods, it's about navigating the view hierarchy, which is possible in every GUI framework I've used so far.

Yeah, but it's pretty much impossible to accidentally do so (as demonstrated in this case).

Objective-C has a lot of reflection built in - you can determine an object's class, methods, and even swap out the implementations of methods at runtime.

So it's pretty trivial to do something like:

    if ([someObject respondsToSelector:@selector(_privateMethodName)]) {
        [someObject performSelector:@selector(_privateMethodName)];
    }
Apple scans for the easy to detect uses of this (i.e., string literals of private methods in the binary), but of course anyone sufficiently determined to get around it, can.

In this case though it's not so much accessing a private method but relying on undocumented and non-guaranteed behavior - walking a view hierarchy looking for a particular view that has no exposed interface. This sort of hackery is highly fragile (it can and does change between releases of iOS), and IMO should not be allowed in a shipping codebase.

Method accessibility is enforced statically, but it's still a message send, so you can override the static checks in a number of ways to send the message anyway.
You can do so in most languages as long as they're dynamically typed or have a reflection API, and Objective-C is dynamically typed.

But in this case private methods aren't exactly involved, the code traverses the views (widgets) hierarchy (which is necessarily public) until it finds an object of a specific class (which comes from a private framework), then it replaces one of the methods to get the behaviour it wants (it actually replaces the class of the object with a subclass overriding that specific method, something many if not most dynamically typed languages allow for, which is way overkill, usually you'd just swap the method — that's called "swizzling" in the objective-c world)

You're not accessing a private method. All UIView subclasses have a method to get the subviews.
Ah thanks, I was thinking they were banning all cordova style apps!
Actually, it rejects apps that use some private methods of UIWebView. Unfortunately this is done in one of the 'main' Ionic plugins and was fine until now and was used in thousands of apps...
Note that it's not starting to, quickly googling it shows they were rejecting applications back in October 2015 on that ground: https://forums.developer.apple.com/thread/22110

The difference may be in the way it's accessed, and that apple added new detectors which trigger on ionic's access pattern.

I suspect that just a simple search for the class name would be sufficient for 99% of existing cases.
Using private APIs in a plugin that other people embed into their apps, and not warning them in advance, strikes me as a reckless thing to do.
Worst of all, not even attempt to hide it. It is very easy to hide such trivial uses of "private" API.
I'd say attempting to hide it would be way worse, it would have been better if they'd clearly noted they're making use of private APIs which could break at any moment in the readme/documentation, but at least you can see it's dodgy by just going through the code.
The way I see it, there are two types of private API use - malign and benign. Malign would be attempting to circumvent iOS security, such as attempting to access disallowed services, attempting to retrieve personal information, abuse, etc. Bening would be use for overcoming Apple bugs or shortcomings in public API. If for the latter, as here, it is not that bad in my book.
Motivation is irrelevant to my comment.

If you're using private APIs in a library, any user of the library is at risk of submission rejection because of the library, the least you can do for them is let them make an at least somewhat informed choice about it, and ideally (if your library is multi-purpose) have them opt in the behaviour requiring private API access.

It's not a question of purpose or of "badness", and really has nothing to do with your book.

Oh, I agree with that completely. Just wanted to distinct between "good" and "bad" use of private API.
Good riddance. If your "app" fits entirely in an UIWebBrowserView, it should be a website.
Did you even read the link?
Another day, another misleading title.
Title is accurate. People don't know what a UIWebBrowserView is, it's their problem.

Hint: It's completely different than UIWebView.

Accurate but misleading, I never said the title was inaccurate.
I cannot see how it is misleading by being accurate. People just need to read the article, then do some research (like a simple Google search).
It's misleading because most people will read it as Apple rejecting hybrid apps like those that use Cordova, which is completely false. In fact, in this very thread someone commented "Good riddance. If your "app" fits entirely in an UIWebBrowserView, it should be a website." Seeing as how a good portion of HN users don't bother clicking through to the actual articles, this title can definitely be categorized as misleading. I myself initially thought it meant Apple was starting to reject hybrid apps, but only upon reading the comments did I realize what the title actually meant.
it's not accurate. Apple did not "start" rejecting. They've been rejecting all apps that use private APIs, including UIWebBrowserView. The title should be "Apps built with Ionic are rejected for using UIWebBrowserView"
No, it's kinda misleading. Saying that everyone should be intimately familiar with your chosen platform is pretty arrogant.
I am not arrogant. For example, I am not familiar with Android intricacies; I would not jump to sensationalist conclusions just from a title without read the article, first and foremost, then doing some basic research. How arrogant is it to expect this of fellow tech people?
In this thread you've been nothing but arrogant. Claiming that everyone should know the intimate details of your platform, and then to google it when they might not even be aware that they don't know, is quite arrogant.

Fix the title.

AFAIK there is another way to do this using categories that has not been banned yet.
Well, you CAN doesn't mean you SHOULD :)
Why is this enforced by a policy rather than code? Why aren't "private APIs" even accessible for third party apps?

(I guess what I'm asking: why hasn't a security mechanism been built in the language/linker/OS for this purpose?)

Do we really need more "security mechanisms" just because people might find something they could do which you didn't expect, but is actually useful for them and allows them to do what they want? A bit of a philosophical point, but I don't think it's a good thing at all if everything anyone ever did required explicit approval from some entity.

Then again, I don't get the culture of strict conformance around iOS (and Apple in general) either...

> Then again, I don't get the culture of strict conformance around iOS (and Apple in general) either…

Really? You don't get why apple would want to avoid software unexpectedly breaking on every OS update (on user devices, to widespread cries of "Apple broke software X") because the dev used an undocumented and unsupported private API which was modified or removed entirely?

The current situation is one in which things are breaking, right? Why do they want things to break now rather than in some hypothetical future?
It is breaking for the developers, not the end users.
> The current situation is one in which things are breaking, right?

No, the situation posted about is one where developers get their application rejected, nothing is broken.

> Why do they want things to break now rather than in some hypothetical future?

1. breakage in private APIs is absolutely not hypothetical

2. rejecting submission is a very different (and much milder) issue than applications breaking on end-user systems

No, the current situation is that developers are finding their updates are being rejected. Because they're being rejected right now while the apps are still being actively maintained, there is a vastly higher likelihood that (as you see happening right here) it'll get immediately noticed and effort expended to resolve it by switching to stable public APIs.

In contrast, if they continued to use private APIs then the app's future failure point becomes entirely unpredictable. The entire point of private vs public isn't security or arbitrary conformance or whatever per se, but rather flexibility. Apple wants to be able to alter internal implementation details as their needs dictate, or feels that a given API is not yet one they're comfortable committing to maintaining intact long term. That means it may get changed not merely in a major OS update but even in a maintenance update. What if everything breaks in iOS 11.2.3 a couple of years from now? Will all of these apps actually still be maintained? Will their developers even still be in business at all? Users would have zero warning, it wouldn't even necessarily happen on a major update where incompatibilities might be more expected.

ObjC (and other dynamic languages) cannot enforce "private" in code, and on OS X there is a long history of private API use, which probably is part of what drove Apple's decisions here. I've used private APIs there myself, at times in the history of its development it's been the only way to accomplish some really cool stuff. But it was absolutely fragile and also resulted in a lot of things that'd break later if not actively maintained, either when Apple removed it entirely or conversely stabilized it and baked it into public APIs/frameworks.

Apple wants to be able to maintain an aggressive OS upgrade schedule and simultaneously have apps continue to work even if they're from many versions back and are no longer actively maintained (which is an important part of getting most people to upgrade). Requiring App Store software to stick to public APIs is one reasonable way to work toward those goals given their constraints.

> it'll get immediately noticed and effort expended to resolve it by switching to stable public APIs.

FWIW there may not be a public API for what they're trying to do.

> ObjC (and other dynamic languages) cannot enforce "private" in code

Even if they could, a root issue here is that it's a UI views tree (so it's possible to access any subview by traversing the views tree), to fix that you'd need to wrap any non-public view object in some sort of opaque view adapter/proxy to prevent anyone getting a handle on them, which would be hard to enforce and would have a runtime cost. Once you've got a handle on it, unless the language has neither reflection nor unsafe observation (e.g. digging through the vtable directly) your private API is toast.

This would not be the first time I've seen a "Can't get there from here" issue in Apple's API design (once upon a time, we tried to render from one process's pixel buffer directly into another process's window prior to 10.6, only to discover that was basically not supported; we got around it by flushing the pixels to a memory-mapped file and then flushing them to the window on the receiving side).

Unfortunately, that's part of the nature of working within Apple's ecosystem---you want store support, you play by their rules. Their rules don't allow your app to exist, you sit on your hands and wait 'til they do. The benefit gained is a bit of consistency of experience on the user's side.

>FWIW there may not be a public API for what they're trying to do.

Of course, I didn't say they'd still be able to accomplish the same thing, after all that's the most common reason one would be interested in using private APIs in the first place. On Mac OS X (as it was back then rather then "OS X") for example at one point there was no good public API for menu bar extras. Apple wanted everyone to use the NSStatusBar API but it was vastly less capable then the private version Apple used, so naturally the latter was rapidly reverse engineered and a whole ecosystem quickly developed. There was actually a bit of an arms race for a while, a preface perhaps to the current iOS situation, because with Jaguar (10.2) Apple added new code specifically to attempt to force the exclusion of all 3rd party menu extras. Apple wised up about that later and just accepted that a bleeding edge would exist, but they were pretty sensitive even very early on to worries that crashing or bugs in 3rd party software that took down soemthing major (like the entire SystemUIServer) would have a major negative effect on perception and adoption of Mac OS X on the whole. Presumably at least some that attitude in turn dates all the way back to the stability horror story that was classic Mac OS in the later years in particular.

On the Mac it was futile and pissed off devs and power users, the block was broken before Jaguar was even GM'd, but it's something they've clearly never entirely forgotten and now under iOS they're using their control to enforce it. Even if functionality is only possible with a private API in the present OS release they'd rather an app be forced to use less but be more sure that it'll work at all in the long term.

Pushing off the breakage to the hypothetical future is how you end up with the Win32 API---Microsoft actually had to go and reverse-engineer their own (C++-implemented) classes into C structs because apps like Photoshop were digging through private structures to squeak a bit of performance out of the system that the APIs didn't directly allow for. When changes in the C++ implicit rules changed the layout of the structures the compiler made, big-name apps would break and Microsoft got into a state where they couldn't release a new OS without "breaking Photoshop."

Apple would rather not be shackled to popular developers cracking open their design to do things that are forwards-incompatible.

Absolutely wrong. Nothing is breaking. Apple simply rejected an app / an update to give the developer a chance to fix the issue that could break at some point in the future.
Because if they break now, before the app is published, then the developer can fix it. Once people have published apps with private api use, it becomes extremely difficult to change those private apis.
The problem of doing this is you end-up with something like the Win32 API, with decade of undocumented methods people called that you must support 10 years after because some critical applications are crashing.
One of the best things about the iOS ecosystem is that people actually update their OS. This allows Apple to add new features, and developers can then develop for them knowing the user base isn't hanging back due to maintaining comparability with legacy code (witness how long it took IE6 to die).
It's amusing to me that people defending very restrictive development frameworks will often talk as if any easement of the policy is a slippery slope to Windows 98. There is no middle ground! Can't do that because PCs sucked in the 90s! (Also implicitly ignores that NT based Windows got a lot better.)
Because widget hierarchies are naturally traversable, and objective-c is a dynamically typed language.
The nature of dynamic language such as Objective C.
Most static language wouldn't help any. Were it java, you could traverse the widgets tree as done here, then use reflection to call whatever method you want once you reached the widget you wanted.
Java is an exception since it was designed to run untrusted bytecode. The Security Manager controls access to reflection and could easily be configured to disallow doing that.
You may be misunderstanding the usage of the word "private". It doesn't mean that these APIs are obfuscated from developers, it's just that they are only meant to be used internally by frameworks created by Apple. Using them directly can result in all kinds of unforeseen behavior because they were never meant to be instantiated or called by arbitrary objects.
Title should probably include information that "UIWebBrowserView" is a private API and not UIWebView.
Or, people should read the article. It's as simple as that.
Not really.

Most users won't read all the finer points in the linked forum thread and comments here and will so remember the wrong, incorrect or incomplete information from the title and maybe make decisions influenced by that in the future.

It's been a while since I did anything iOS related. I didn't remember exactly what the class was named, and clarifying this in the title would have been helpful. It's not a question of how well I read.
Read the article??

I'm still pretty new to HN, but on other forums like this, that suggestion is outright heresy.

Third party developers should conform their code to Apple's public API's, plain and simple. Hacking around in Apple's private APIs is just begging for a crash or an incompatibility when Apple changes something under the hood.
For developers not actually writing code that does this but rely on the developers of tools like Cordova/Ionic.., are they expected to learn the language the apple code is written in in order to do that?

At some point the burden falls to the abstraction author for the mistake, and the abstraction user chalks one up in "well I couldn't have foreseen that happening"

I'd say the take-away message here is: yes, learn a little about the environment you are working with & my guess the take-away is rather going to be "Next time I'll void abstraction layer X".
One thing I've never understood about situations like this: why doesn't the platform manufacturer just make it technically impossible to call private classes/methods without some kind of signed key? White list the OS classes/methods that all apps are generally allowed to call, and require some kind of signed key to call anything else.

Why try to fix this with policy rather than some technical block? I am sure I am missing something here, so any insight is greatly appreciated.

I'm sure that'd be preferable, there's just no easy way to do that with objective-c, because of how dynamic it is. You can pass any message (call any function by name) on any object. Changing that would be a really significant effort.
Yeah it's kind of impossible when you basically have full control of the method dispatch system at runtime.
That seems like a paranoid waste of engineering resources. Keep in mind Apple likely needs to access these components in the same address space as an app, and this fool's errand will only make everybody's work more annoying. Also keep in mind that 99% of internal methods and objects will never lead to a harmful "misuse", and you can't predict ahead of time which one will be a real problem.
>That seems like a paranoid waste of engineering resources.

And if someone _really_ wanted to get around it they could by simply bypassing the "key checking" trampoline and jumping directly to where the wanted to end up in the first place. (Obj-C is still C).

That would technically make it a little bit harder for them to let Facebook use all their internal APIs.
One reason is that it's only App Store apps which are subject to individual approval by Apple.

If you are an individual developer simply wanting to learn/dissect/play with the inherent flexibility of the obj-c runtime, restricting access to private methods/subclasses would be incredibly counterproductive and demoralising.

If you are a registered Enterprise Developer (which requires a DUNS number, a phone call with Apple Dev Relations, and a few hundred dollars) you gain additional distribution options. For example you can sign your own apps and distribute them within your organisation "over-the-air" using emailed links or a custom web portal.

In general I think Apple are more concerned with the integrity of their mass-market public platform (App Store etc) -- avoiding vast numbers of App Store apps all failing at once because they make some internal framework changes -- than they are about other use cases.

Apple is trying to make it more difficult to link to private API in Xcode 7.3 but objc doesn't help https://github.com/kif-framework/KIF/issues/770
It wouldn't have done much in this case anyway, since that's just removing the ability to link private frameworks, while this is a private class of UIKit which is public...
There are ways to get around this technically where Apple couldn't detect what you're doing, but definitely not recommend. I wonder if these rejections are a nod to Apple doing something with the underlying Web Browser View? Apple may be trying to prevent what would potentially become crashes by changing that private API. Start the heads up no so by the time Sept comes around with iOS 10, they can remove / refactor that class.
Hey all, author of the plugin for Ionic here. What a way to start a Friday morning!

So this was apparently a ticking time bomb, since we were directly using the name "UIWebBrowserView" to override that method at runtime, which is trivially found by Apple.

For anyone asking, "Why not use public APIs?", the answer is: because there are none for removing the keyboard accessory bar in a web view. At least not when the plugin was written, and as far as I know, that is still the case.

For hybrid apps, removing the keyboard accessory bar to look like native is fairly common practice. At the time the code was "written" (I'll use that term loosely), I didn't know much about Objective C and went with what worked. And it has worked, for the past couple of years, until today.

For the time being, I've removed the private API use until we find a solution that doesn't get automatically rejected (by not using the name of a private API directly, for example).

Thanks to everyone who brought this to our attention, and sorry for the headache!

Just wanted to add - since we don't expect there to be a public API for this, even if we do find a solution that doesn't get auto-rejected, we'll be much more up front about our use of private APIs so people are aware of the risks of something like this happening at some point.
Why does this affect you but not affect Meteor and Phonegap, both of which also use a hybrid approach?
Nobody else wants to do what they are doing. They are removing a native control that's built into the keyboard (above it actually), probably to put in their own custom one I'm guessing. While apps are getting rejected because of accessing a private API to remove it, I think an even better reason to reject these apps is because users expect to have the native keyboard accessory be part of the keyboard control.
Just to clarify, this is not an Ionic-specific issue: It's a Cordova plugin maintained by Ionic, so it will affect all apps using the plugin, including Meteor-Cordova and PhoneGap apps. The fix is the same for everyone: update the plugin!

It's installed by default on all Ionic apps, so it's safe to say a majority of Ionic apps will be affected by it.

Could you do something like this?

1. Add a text input as another view outside the UIWebView

2. Translate the frame of the input off screen so it's not visible

3. Wire up the input with the webview to mirror focus with the webview text input (maybe a combination of UIWebView executing JS, and the web content passing redirects back to the webview delegate that get interrupted by webView:shouldStartLoadWithRequest:navigationType:)

4. Pass the text input back into the webview from the text view delegate method textView:shouldChangeTextInRange:replacementText:

This is something we haven't explored enough yet, but would be awesome to get working since it would allow us to support all UITextInputTraits and get rid of the accessory bar. We had a simple PoC, but I seem to recall there being some weird edge cases.

Definitely worth looking into more though!

In case anyone is wondering, this plugin hides the "accessibility bar," i.e. the keyboard bar that appears when you tap on a form element in iOS: https://nolanwlawson.files.wordpress.com/2016/03/accessibili...

The accessibility bar is really annoying for hybrid app developers, because it's often not needed, and just takes up valuable screen real estate. As one of my coworkers put it, it has no purpose other than to announce, "Hi! I'm a WebView!"

Well, that's what happens when you don't write native apps.
I'm having trouble thinking of cases when it isn't needed, do you have a simple example?
Let's say you have one big contenteditable, so there's only one field, and it's pointless to have "previous," "next", and "done" buttons. Or just any situation where you don't think the user needs to be able to switch left and right and can tap instead. Or situations where you want to make your own UI instead of using the default Safari UI.
But the "Done" button would hide the keyboard so you can see more of the text to review it, right? It just seems user-hostile to remove system features that otherwise always appear.