Can I preload the web content for Safari View Controller? - objective-c

I can create Safari View Controller without problem:
let svc = SFSafariViewController(URL: NSURL(string: remote_url)!, entersReaderIfAvailable: true)
self.presentViewController(svc, animated: true, completion: nil)
Is there any way I can preload the URL before I present the view controller to the user?
For example, I can preload the URL (web content) in the background first, and after the user clicks on something, I can show the Safari View Controller with the content right away. The user will feel the page loading is faster or instant.
P.S. Workarounds/hacks are also acceptable. For example, using cache or starting the view controller in background, etc.
EDIT: please consider SFSafariViewController only.

Here is a solution.
Obviously, if you click on the button right away you'll see the loading.
But basically, I load the Browser and put the view behind another one and I put a button in this other view.
When you press the button, the browser is bring to the front, already loaded.
The only problem here is that I'm not using any transition but that's one solution at least.
import UIKit
import SafariServices
class ViewController: UIViewController {
var svc = SFSafariViewController(URL: NSURL(string: "https://microsoft.com/")!, entersReaderIfAvailable: true)
var safariView:UIView?
let containerView = UIView()
let btn = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
//let tmpView = svc.view
addChildViewController(svc)
svc.didMoveToParentViewController(self)
svc.view.frame = view.frame
containerView.frame = view.frame
containerView.backgroundColor = UIColor.redColor()
safariView = svc.view
view.addSubview(safariView!)
view.addSubview(containerView)
btn.setTitle("Webizer", forState: UIControlState.Normal)
btn.titleLabel!.textColor = UIColor.blackColor()
btn.addTarget(self, action: "buttonTouched:", forControlEvents: .TouchUpInside)
btn.frame = CGRectMake(20, 50, 100, 100)
containerView.addSubview(btn)
view.sendSubviewToBack(safariView!)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonTouched(sender: AnyObject) {
view.bringSubviewToFront(safariView!)
//self.presentViewController(svc, animated: true, completion: nil)
}
}

Sadly this behaviour is not supported with the current implementation of SFSafariViewController. I would encourage filing a radar with Apple to add support for this behaviour but like others have suggested your best bet is to use WKWebView and start loading before its added to the hierarchy.
I came across a lovely radar from Twitter that actually mentions exactly what you're asking for. I think you might find the following requests useful:
High Priority:
- Ability to warm the SFSafariViewController before actually presenting it with a URL, URL request, HTML data or file on disk
- Currently, are investing heavily into warming the shared URL cache for high priority Tweets so that if the user hits that Tweet we
will open UIWebView (sadly not WKWebView) with that pre-cached web
page. If we could just warm an SFSafariViewController with the
desired link, this would eliminate an enormous amount of effort on our
end.
You can see in their implementation they simply cache responses using UIWebView since WKWebView seems to obfuscate the caching semantics a bit. The only risk is that UIWebView is a likely candidate for deprecation as you see in their docs "In apps that run in iOS 8 and later, use the WKWebView class instead of using UIWebView."
So unfortunately it seems that their are many hoops you need to jump through to get this all going so your best bet for now is to just pester Apple and dupe Twitters radar.

You could try using a http cache, but I don't think it would work as the Safari View Controller is working as a separate process (probably the same as Safari), so that's why it e.g. circumvents ATS.
The only way I can think of this working is to somehow force the user's Safari to load it? openURL: or adding to Reading List maybe? This doesn't sound like a viable solution.
You can always experiment with custom presentation of the view controller, attach it the view hierarchy, trigger appearance events, but set its frame to CGRectMake(0,0,1,1) or attach it somewhere off-screen, then wait a while and represent it with a correct frame.

you can download the web page using the following code . and represent it with the help of svc
let data:NSData?
do {
let weatherData = try NSData(contentsOfURL: NSURL(string: remote_url)!, options: NSDataReadingOptions())
data = weatherData
print(weatherData)
} catch {
print(error)
}
and load it when you needed in the svc

While it's technically possible to use the solution above to achieve what you're asking, this may not pass App Store review. Per the SFSafariViewController docs:
In accordance with App Store Review Guidelines, this view controller must be used to visibly present information to users; the controller may not be hidden or obscured by other views or layers. Additionally, an app may not use SFSafariViewController to track users without their knowledge and consent.

Related

How do I intercept downloads from a WKWebView?

I am making a Cocoa app that involves users selecting pictures from online for use within the app. I am also looking to collect contextual metadata from those downloads, such as the host the image came from, the website the user was visiting, the exact MIME type in the response headers, etc.
Basically I want to curate my user across the internet, downloading images and metadata into that user's account as I go. Until today I thought this would be impossible.
But just recently I was toying with a WKWebView and I tried right clicking on an image. I saw this...
Is there any way I could connect to that Download Image button, and get notifications when its clicked?
I did figure out how to make "Download Linked File" work and its a doozy that will not fit in a SO answer: https://github.com/kfix/MacPin/commit/9e5f925819f7f54ef29baff1e90783b820e683a3
However implementing those private delegate functions doesn't seem to allow "Download Image" to signal my WkWebView app in any way.
You can intercept those non-working "Download Image" and "Download Linked File" menu items by subclassing the WKWebView class and implementing the willOpenMenu method like this:
class MyWebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if menuItem.identifier == "WKMenuItemIdentifierDownloadImage" ||
menuItem.identifier == "WKMenuItemIdentifierDownloadLinkedFile" {
menuItem.action = #selector(menuClick(sender:))
menuItem.target = self
}
}
}
func menuClick(sender: AnyObject) {
if let menuItem = sender as? NSMenuItem {
Swift.print("Menu \(menuItem.title) clicked")
}
}
}
Instead of this you can also simply hide the menu items with menuItem.isHidden = true
Detecting the chosen menu item is one thing, but knowing what the user actually clicked in the WKWebView control is the next challenge :)

tvOS: Focus first button as preferred focus view when buttons are in horizontal order

I have been trying to focus the first button but focus engine takes the third button right below the tab bar as the first item to be focussed. I tried with preferred focus view but found that when i place the buttons in vertical order then preferred takes the preferred view to be focussed but when i placed all the buttons in horizontal plane it always takes the third button.The other approach i can think of if Focus Guide but i wonder how that will work in this scenario?
override weak var preferredFocusedView: UIView? {
get {
return thrirdButton
}
}
It happens because focus engine takes the nearest possible focus element as 1st element as you can see from the picture attached.I have attached the context screenshot for the view controller. Any help or clue to solve this will be appreciated.
Solution :
We need to add focus guide just above button 3 and redirect it to button one when the focus guide is focussed.
private var focusGuide = UIFocusGuide()
self.view.addLayoutGuide(focusGuide)
self.focusGuide.bottomAnchor.constraintEqualToAnchor(self.button3.topAnchor).active = true
self.focusGuide.leftAnchor.constraintEqualToAnchor(self.button3.leftAnchor).active = true
// Width and height
self.focusGuide.widthAnchor.constraintEqualToAnchor(self.button3.widthAnchor).active = true
self.focusGuide.heightAnchor.constraintEqualToAnchor(self.button3.heightAnchor).active = true
focusGuide.centerXAnchor.constraintEqualToAnchor(self.button3.centerXAnchor).active = true
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
self.focusGuide.preferredFocusedView = button1
}
Try adding a UIFocusGuide just above the button rows. In that case, before reaching the button, it will hit your focus guide in which you can redirect the preferredFocusedView to that particular button. The code to redirect is to be done by overriding didUpdateFocusInContext method.
Assuming you have setup the focus guide in viewDidload, the following is a sample,
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
self.focusGuide.preferredFocusedView = self.mypreferredbutton
}
TO initialise a focus guide ( Do the addition of guide to view in viewDidLoad)
var focusGuide = UIFocusGuide()
self.view.addLayoutGuide(self.sidefocusGuide)
self.focusGuide.leftAnchor.constraintEqualToAnchor(self.placerLabel.leftAnchor).active = true
self.focusGuide.rightAnchor.constraintEqualToAnchor(self.placerLabel.rightAnchor).active = true
self.focusGuide.topAnchor.constraintEqualToAnchor(self.audioTable.topAnchor).active = true
I too faced a similar issue but somehow able to do it through this. The preferredFocusedView approach will also work, but you have to do lot of circus by updating some reference variable and then calling setneedsfocusupdate for that view. Try the focus guide way. Hope it helps.
EDITED:
I have added code how to setup the guide. In this case I have put the guide on the right side of my view. So, whenever your focus hits the guide, it redirects to the preferredfocusguide view that you want the focus to go to in the didUpdateFocusInContext. "There is only one view in focus at anytime.", remember thes ? So, the moment it hits the guide, the overriden method gets hit which in turn moves your focus to the guide's preferred view. For examples, you can refer to Apple's UIKitCatlog app for tvOS and here is one link explaining the same.
The best way to give the initial focus to your preferred view is by using preferredFocusEnivronments.
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [firstButton]
}
preferredFocusEnvironments will be called before didUpdateFocus, that way you can inform the system, where you want the focus to be redirected.

SpriteKit - Preloader using GCD

I am looking for the instructions of how to make an preloader using GCD, but no luck for now. I would like to learn how to preload all the textures and emitters in the background, before scene is initialized. And if possible, update the progress bar animation based on amount of data loaded.
I am looking at Apple's adventure game, and there is an example for similar problem. Actually, the first part is solved. But I can't find anywhere about updating preloader based on the amount of data loaded, so any instructions would be helpful. There must be some obvious way,to calculate, or get info about data loaded in the background queue.
I don't do any progress bars since my levels take about a second to load. I have a seamless transition between levels. Before I was doing things this way my game would freeze for a moment in a transition because it had to load all the resources on the same thread that my game was running
I create a static method in my class to preload resources.
// config is a struct with many parameters
class func createResources(config: HyperSceneConfig, withCompletion: (scene: HyperScene) -> ()){
// load resources on other thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), {
let hyperScene = HyperScene(config: config)
// callback on main thread
dispatch_async(dispatch_get_main_queue(), {
// Call the completion handler back on the main queue.
withCompletion(scene: hyperScene)
});
})
}
my scenes init
init(config: HyperSceneConfig) {
super.init(size: config.size, viewController: config.viewController)
self.backgroundColor = SKColor.blackColor()
self.levelTime = 200
// this is where i do all my setup before the scene is presented
self.setupScene()
}
this is how I present my scene in my gameViewController.
You could do the same sort of thing in a scene to load the next level. This is great because you can create a seamless transition between levels since everything is loading on a different thread.
HyperScene.createResources(hyperConfig, withCompletion: {
scene in
self.scene = scene
self.scene.scaleMode = .AspectFill
self.skView.presentScene(self.scene)
self.view.addSubview(self.skView)
})

How to hide the iOS 8 Today Widget when there is nothing to display?

I'm using the today widget for iOS8 to display information relevant to the current day. The problem is I don't want to display the widget/section at all if there are no relevant messages to show.
I know it must be possible as the BA app does it (it only shows the widget when there is a flight, the rest of the time its not visible at all). I just cant figure out a way to achieve this behaviour.
Does anyone know how this can be done?
I found the way to do this is using the NCWidgetController. This allows you to easily specify when the today widget should be displayed based on whatever criteria you see fit.
Simply add the following into your viewDidLoad method (or anywhere that will be called when the widget reloads) in the today widget view controller and it will work:
BOOL hasDataToDisplay = NO;
NCWidgetController *widgetController = [NCWidgetController widgetController];
[widgetController setHasContent:hasDataToDisplay forWidgetWithBundleIdentifier:#"com.my-company.my-app.my-widget"];
Apple Docs: https://developer.apple.com/library/ios/documentation/NotificationCenter/Reference/NCWidgetController_Class/index.html#//apple_ref/occ/cl/NCWidgetController
WARNING: The NCWidgetController cannot be reset from the widget itself once you have set there is no content to display. In other words once you set it to NO then there is no going back unless you trigger it from the parent/containing app.
In the widget's ViewController's viewDidLoad method add the following:
BOOL DisplayWidget = NO;
[[NCWidgetController widgetController] setHasContent:DisplayWidget
forWidgetWithBundleIdentifier:#"<widget's bunder identifier>"];
This will disable the widget from showing.
To enable it again, you must do that from the containing app using the same line passing YES to setHasContent parameter. Make sure to add the necessary imports to the containing app in the ViewController which will re-enable the widget:
#import <NotificationCenter/NotificationCenter.h>
#interface ViewController () <NCWidgetProviding> {...}
[Check out page 41 of the documentations for widgets
https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensibilityPG.pdf ]
The approach which I used, though not perfect and has a small remnant in Notification Center, but worked for me:
In viewDidLoad() set preferred content size height to 1:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view from its nib.
preferredContentSize = CGSizeMake(0, 1)
view.setNeedsLayout()
}
then when widget updates, gets real height and set it:
var data: NSData?
func updateData() {
// fetch data
if let data = data {
let viewHeight: CGFloat
// UI preperation and initialize viewHeight var
preferredContentSize = CGSizeMake(0, viewHeight);
} else {
preferredContentSize = CGSizeMake(0, 1);
}
}
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)) {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResult.Failed
// If there's no update required, use NCUpdateResult.NoData
// If there's an update, use NCUpdateResult.NewData
updateData()
completionHandler(data != nil ? NCUpdateResult.NewData : NCUpdateResult.NoData)
}
It is better use
+ (instancetype)widgetController
then call
- (void)setHasContent:(BOOL)flag forWidgetWithBundleIdentifier:(NSString *)bundleID

scrollViewWillBeginDecelerating alternative, too slow for tracking page change

I am currently using pagination in a UIScrollView, and for tracking any page change I use;
-(void) scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
if (_previousContentOffset > _pageContainer.contentOffset.x)
{
NSLog(#"Less");
_currentPageIdx--;
}
else if (_previousContentOffset < _pageContainer.contentOffset.x)
{
_currentPageIdx++;
NSLog(#"More");
}
}
Now the problem is that, this method isn't tracking the touch fast enough, so when this method is called, the user can be 3 pages along if he/she is paging like a maniac. I tried setting
_pageContainer.decelerationRate = UIScrollViewDecelerationRateFast;
but that didn't make the tracking much faster.
Is there a solution or alternative for this?
Use scrollViewDidScroll: instead. It is being called continuously while the user scrolls.