Apple starts rejecting apps that use UIWebBrowserView
forum.ionicframework.com
forum.ionicframework.com
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?
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.
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
So removing this code would mean that all Ionic apps would display this thing here on all keyboards: https://raw.githubusercontent.com/clrung/PrevNextInputAccess... Did I get this right?
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?
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 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.[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
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.
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.
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)
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!
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.
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:
Definitely worth looking into more though!
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.
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!"
The difference may be in the way it's accessed, and that apple added new detectors which trigger on ionic's access pattern.
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"
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.
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).
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.
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.
I'm still pretty new to HN, but on other forums like this, that suggestion is outright heresy.
Hint: It's completely different than UIWebView.
Fix the title.
(I guess what I'm asking: why hasn't a security mechanism been built in the language/linker/OS for this purpose?)
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?
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
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.
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.
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.
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.
Apple would rather not be shackled to popular developers cracking open their design to do things that are forwards-incompatible.