SwiftUI Link to open Bundle files in browser - url

You can get the browser to open a URL with one line of code...
Link("Info", destination: URL(string: "https://my.web.address/info.pdf")!)
You can compile and build this....
Link("Info", destination: Bundle.main.url(forResource: "info", withExtension: "pdf")!)
No errors or crashes, but it does nothing. The Bundle.main.url() reply makes sense within the application, but does not work in the browser, which is not in the application. This was a security feature to stop people pulling data from your application, according to an answer from about 10 years back.
I have written a WKWebView page within my application to display the file. This works, but it took a lot more than one line. I would have actually preferred my original solution, if there had been some way for me to mark certain files as suitable for public release. As well as the manual, I have a PDF test chart image, and a table of CSV data that I would like to make public like this. I could use a button in the app to open them in the browser, and the user could then save a copy when and where they wanted. The user gets to re-use their browser skills, and I write one line of code per file. Win-win, right?
Can we open a Bundled file in the browser using a Link using some trick I haven't found yet? Is there a good reason why we must not do this?
Apologies if you have read (and possibly deleted) a question like this earlier today. I wrote this question, and I thought I had submitted it, but I can't find it now. So here it is again.
FWIW, here's my WKWebView page code...
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
typealias UIViewType = WKWebView
var webView: WKWebView
init() {
webView = WKWebView(frame: .zero)
webView.load(URLRequest(url: Bundle.main.url(forResource: "ByEye", withExtension: "pdf")!))
}
// Needed to make view UIViewRepresentable
func makeUIView(context: Context) -> WKWebView { webView }
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
struct ManualView: View {
var webView = WebView()
var body: some View {
webView
}
}
struct ManualView_Previews: PreviewProvider {
static var previews: some View {
ManualView()
}
}
I found a partial solution. You can add text or images to the pasteboard, and then paste the data into an e-mail or other message. This does not work for a general file type, but it was enough to get me where I wanted.
See questions/74784691/swiftui-sharing-application-data-using-pasteboard

Related

How to Deep Link Widget with data pulled from Core Data?

I'm trying to get deep linking working for my app's widgets. The widgets simply display an image from a Core Data entity: Project. I'd like for when a user taps on the widget, it opens up the ProjectDetailView() that the image is a member of.
Everything I read says I should be able to add Link(destination: url) but I'm not getting anywhere with that. In my app, project is pulled from Core Data, but it has no url attribute. I feel like it doesn't need one based on the examples I've seen?
The below code gives me the error Value of type 'FetchedResults<Project>' has no member 'url' which is true.. it doesn't. I don't understand how to get the url to the ProjectDetailView() the image is included in. Thanks so much for taking a look!
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct WidgetEntryView : View {
var entry: Provider.Entry
#State var projectImage1: Data = .init(count: 0)
#FetchRequest(entity: Project.entity(), sortDescriptors: []) var project: FetchedResults<Project>
var body: some View {
if let randomProject: Project = project.randomElement() {
Link (destination: project.url) {
Image(uiImage: UIImage(data: randomProject.image1 ?? self.projectImage1) ?? UIImage(imageLiteralResourceName: "icon"))
.resizable()
.aspectRatio(contentMode: .fill)
}
}
}
}
If you look at the url in their CharacterDetail.swift it is something like "game:///spouty" game to represent the app the :/// to conform to url and something unique.
So all you need to get a url for for your file is to follow the same pattern.
yourApp:///anyUniqueVariable
You can achieve this in an extension of your object class with a computed variable.
extension Project{
var url: URL{
return URL(string: "yourApp:///\(self.anyUniqueVariable)")
}
}
You keep saying you have a CoreData object that isn't really relevant because the actual connection is made in with SwiftUI and how you "trigger" your NavigationLink to the View that displays your object.
If you look at the ContentView.swift you'll see
.onOpenURL(perform: { (url) in
self.pandaActive = url == CharacterDetail.panda.url
self.spoutyActive = url == CharacterDetail.spouty.url
self.eggheadActive = url == CharacterDetail.egghead.url
})
They are using the NavigationLink init with isActive and triggering the making it true when the "received url == the characters url".
You could use that method (might not be the best since you likely have more than 3 objects) or the init with NavigationLink that has selection and set the tag/selection variable to the url you received in openURL.
It just depends on how you want to trigger your NavigationLink.

SwiftUI DocumentGroup and switching to background

I have made an app based on the new SwiftUI multi platform target for a "Document based app".
However, I face weird issues. As long as an app is in the foreground, it works just fine. If it is moved to the background by task switching, and then again to the foreground, mutations are being saved to the document, but the SwiftUI Views don't receive mutations. So whenever you press a button in the UI that mutates the document you see nothing happening while the mutation is there once you reload the document from disk.
So i am thinking, I use ObservedObjects, they probably get kicked out of memory once I move to the background. could this be the cause of my bug?
But then I added a print line to the App struct.
import SwiftUI
#main
struct MyApp: App {
fileprivate func myLogging(_ file: FileDocumentConfiguration<MyDocument>) -> some View {
print("""
IT IS CALLED
""")
return MainView().environmentObject(BindingWrapper(file.$document))
}
var body: some Scene {
DocumentGroup(newDocument: MyDocument()) { (file) in
return myLogging(file)
}.commands { AppCommands() }
}
}
and guess what... this print always executes just before a mutation is being rendered. Which makes sense. because file.$document is a binding, and if you do a mutating action, the binding will warn Apple that the file is dirty, but it will also invalidate the entire hierarchy. This logging will still print once the bug has occurred!
So on the line MainView().environmentObject(BindingWrapper(file.$document)) I assume everything is created from scratch. BindingWrapper is a custom class I made to convert a binding in an observable object. And this is one of the objects I worried about, that they might be freed. but if they are created newly.... they should be always there, right?
And by the way, this object is owned by the environment. So it should not be freed.
So, now I am stuck. is Apple doing some clever caching on bindings / ObservedObjects which will inject old objects into my view hierarchy even though I think everything is created newly?
Try moving any wiring/instantiation to the first view of the document group. If that view houses StateObjects you expect to share the lifetime of the document window, they will not be rebuilt.
In the example below, a WindowStore is housed as an #StateObject as described. A RootStore housed in App creates the WindowStore, which includes vending services and registering it in a managed array of windows. Either could enable your logging service. (For me, that array helps WindowGroups operate on a specific document when #FocusedValue would fail (i.e., the top-most document is no longer the key window).)
#main
struct ReferenceFileDoc: App {
#StateObject var root: RootStore
var body: some Scene {
DocumentGroup { ProjectDocument() } editor: { doc in
DocumentGroupRoot(
window: root.makeWindowStore(doc.document),
factory: SwiftUIFactory(root, doc.document)
)
.environmentObject(doc.document)
.environment(\.documentURL, doc.fileURL)
.injectStores(from: root)
}.commands { Menus(root: root) }
.... other scenes ...
struct DocumentGroupRoot: View {
#EnvironmentObject var doc: ProjectDocument
#Environment(\.undoManager) var undoManager
#Environment(\.documentURL) var url
#StateObject var window: WindowStore
#StateObject var factory: UIFactory
var body: some View {
passUndoManagerToDocument()
factory.reference(window)
return DocumentWindow(vm: factory.makeThisVM()) // Actual visible window
.focusedValue(\.keyWindow, window)
.focusedValue(\.keyDocument, doc)
.onAppear { /// Tasks }
.reportHostingNSWindow { [weak window] in
window?.setWindow($0)
}
.onChange(of: url) { [weak window] in window?.setFileURL($0) }
.environmentObject(/// sub-state stores from WindowStore)
.environmentObject(window)
.environmentObject(factory)
}
}

Perform a deeplink from SwiftUI widget on tap

I have a simple widget (medium-sized) with two texts, and what I want is to be able to perform a deep link to lead the user to a specific section of my app, but I can't seem to find a way to do so.
The view I have written (which is very simple):
HStack {
Text("FIRST ITEM")
Spacer()
Text("SECOND ITEM")
}
I have already tried to replace
Text("SECOND ITEM")
with
Link("SECOND ITEM destination: URL(string: myDeeplinkUrl)!)
but it doesn't work either.
In the Widget view you need to create a Link and set its destination url:
struct SimpleWidgetEntryView: View {
var entry: SimpleProvider.Entry
var body: some View {
Link(destination: URL(string: "widget://link1")!) {
Text("Link 1")
}
}
}
Note that Link works in medium and large Widgets only. If you use a small Widget you need to use:
.widgetURL(URL(string: "widget://link0")!)
In your App view receive the url using onOpenURL:
#main
struct WidgetTestApp: App {
var body: some Scene {
WindowGroup {
Text("Test")
.onOpenURL { url in
print("Received deep link: \(url)")
}
}
}
}
It is also possible to receive deep links in the SceneDelegate by overriding:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)
You can find more explanation on how to use this function in this thread:
Detect app launch from WidgetKit widget extension
Here is a GitHub repository with different Widget examples including the DeepLink Widget.
Also, you can do it using AppDelegate (if you not using SceneDelegate):
.widgetURL(URL(string: "urlsceheme://foobarmessage"))
// OR
Link(destination: URL(string: "urlsceheme://foobarmessage")!) {
Text("Foo")
}
Set this code within AppDelegate
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
let message = url.host?.removingPercentEncoding // foobarmessage
return true
}
See docs on: Respond to User Interactions
When users interact with your widget, the system launches your app to handle the request. When the system activates your app, navigate to the details that correspond to the widget’s content. Your widget can specify a URL to inform the app what content to display. To configure custom URLs in your widget:
For all widgets, add the widgetURL(_:) view modifier to a view in your widget’s view hierarchy. If the widget’s view hierarchy includes more than one widgetURL modifier, the behavior is undefined.
For widgets that use WidgetFamily.systemMedium or WidgetFamily.systemLarge, add one or more Link controls to your widget’s view hierarchy. You can use both widgetURL and Link controls. If the interaction targets a Link control, the system uses the URL in that control. For interactions anywhere else in the widget, the system uses the URL specified in the widgetURL view modifier.
For example, a widget that displays details of a single character in a game can use widgetURL to open the app to that character’s detail.
#ViewBuilder
var body: some View {
ZStack {
AvatarView(entry.character)
.widgetURL(entry.character.url)
.foregroundColor(.white)
}
.background(Color.gameBackground)
}
If the widget displays a list of characters, each item in the list can be in a Link control. Each Link control specifies the URL for the specific character it displays.
When the widget receives an interaction, the system activates the containing app and passes the URL to onOpenURL(perform:), application(_:open:options:), or application(_:open:), depending on the life cycle your app uses.
If the widget doesn’t use widgetURL or Link controls, the system activates the containing app and passes an NSUserActivity to onContinueUserActivity(_:perform:), application(_:continue:restorationHandler:), or application(_:continue:restorationHandler:). The user activity’s userInfo dictionary contains details about the widget the user interacted with. Use the keys in WidgetCenter.UserInfoKey to access these values from Swift code. To access the userInfo values from Objective-C, use the keys WGWidgetUserInfoKeyKind and WGWidgetUserInfoKeyFamily instead.

WKWebView in iOS app takes a long time to load

I have a UIView that has a WKWebView as a subview. The web view shows up all right, but it takes a long time (more than a second) to display the HTML document. The document is a local file (an HTML file in the project) so there is no Internet latency, and it's a relatively simple HTML document. The HTML document does have eight small images on it, but there is a similar problem with another HTML document that doesn't have any images.
Here's the code that loads the HTML document into the web view:
override func viewDidLoad() {
super.viewDidLoad()
let localHtmlFile = Bundle.main.url(forResource: "place", withExtension: "html");
let request = URLRequest(url: localHtmlFile!);
webView.load(request);
}
I am using Xcode 9.3, Swift 4.1, and iOS 11.2.
The delay happens every time I go to that screen. If it isn't possible to prevent the delay the first time, is it possible to keep the web view around so that the delay only happens once?
Apparently the delay is caused by the time it takes to make a new instance of WKWebView, not the time it takes to load an HTML document. To avoid that delay I figured out a way to reuse a web view.
First I removed the web view from the storyboard scene so that a new web vew wouldn't be created every time the view was loaded. I made a generic view named container that is the same size that I wanted the web view to be.
Then I made a static variable to keep a pointer to the web view:
static var webView: WKWebView? = nil
In my case this static variable is in a class called GameController.
Next I changed the code to check to see if the static webView variable is nil. If webView is nil, the code creates a new web view and sets the static variable to point to that web view. Then the code programmatically adds the web view as a subview of a container view in the storyboard scene.
To set up the storyboard and write this code I used the explanation on the following web site:
http://www.onebigfunction.com/ios/2016/12/14/iOS-javascript-communication/
The basic code in the view controller for the scene that uses the web view (WebViewController in my code) looks like this:
override func loadView() {
super.loadView()
if GameController.webView == nil {
var webFrame = self.container!.frame
webFrame.origin.x = 0
webFrame.origin.y = 0
let config = WKWebViewConfiguration()
webView = WKWebView(frame: webFrame,
configuration: config)
GameController.webView = webView
} else {
webView = GameController.webView
}
self.container!.addSubview(webView)
}
In my case, I wanted to send information from JavaScript code in the web view to Swift code in my app, so I had to work more with configurations. I also wanted the web view to be transparent, so I added a statement to do that.
override func loadView() {
super.loadView()
if GameController.webView == nil {
var webFrame = self.container!.frame
webFrame.origin.x = 0
webFrame.origin.y = 0
let config = WKWebViewConfiguration()
config.userContentController.add(self, name: "scriptHandler")
webView = WKWebView(frame: webFrame,
configuration: config)
webView.isOpaque = false
GameController.webView = webView
} else {
webView = GameController.webView
webView.configuration.userContentController.removeScriptMessageHandler(
forName: "scriptHandler")
webView.configuration.userContentController.add(self,
name: "scriptHandler")
}
self.container!.addSubview(webView)
}
Originally I only set the script handler when I first made the web view, but that didn't work. Apparently a new view controller object was made each time the scene was loaded, so the old script handler didn't work. This code deletes the script handler that pointed to the old view controller and adds a script handler that points to the new view controller.
Just keep a reference to the ViewController, perhaps in a parent controller, app delegate, or even a singleton/global. It should be somewhat faster on subsequent loads.

How to resize a Maxpreps HTML widget to fit the UIWebView?

I am trying to use the Maxpreps widget (http://www.maxpreps.com/widgets/createwidget.aspx) in my app to show school sports updates.
I made a html file in xcode and pasted the code provided from Maxpreps. I created a webview and used the code on the view controller.
#IBOutlet weak var sportsTest: UIWebView!
sportsTest,loadRequest(NSURLRequest(URL: NSURL(fileURLWWithPath: NSBundle.mainBundle().pathForResource("widgetcode", ofType: "html")!)))
The problem is when the code is being shown it wont fit the UIWebView properly.
The issue you are having with the HTML not fitting in the UIWebView is not simple to resolve, but I can give you a hack that will get your content on the screen.
The problem is that the web page from Maxpreps loaded from the URL in their widget isn't designed to fit completely on an iPhone in portrait orientation. However it is a responsive HTML page so that is the good news.
First, you don't need to load the script tag or write an HTML file since all that gives you is a link that you need to click, right? You don't want that link, you want the content of what is behind the link, right? Hopefully!
This view controller implementation should work fine:
import UIKit
class ViewController: UIViewController {
#IBOutlet var webView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
// This will force the UIWebView to downscale and fit your page, but
// it is hacky because 91% of original size might not always be the
// right amount. This is why I said it is hard.
self.webView.scrollView.minimumZoomScale = 0.3
self.webView.scrollView.maximumZoomScale = 2.0
self.webView.scrollView.setZoomScale(0.91, animated: false)
}
override func viewWillAppear(animated: Bool) {
if let url = NSURL(string: "http://www.maxpreps.com/local/team/home.aspx?gendersport=boys,baseball&schoolid=45823724-55bc-4d89-926b-b1974b3d8e36") {
let request = NSURLRequest(URL: url)
webView.loadRequest(request)
}
else {
println("Failed to create URL")
}
}
}
The above works, but better solutions IMHO would be to:
1) Ask Maxpreps to fix their responsive HTML page so that it renders on an iPhone in portrait orientation
2) Ditch HTML altogether and query the information you need using a REST API if they make one available, and then write a native non-HTML screen
3) Attempt to get a dynamic scaling solution in place that works. These are prone to failure based on my experience
Here is what I see on my simulator when I run it:
Another example:

Resources