iOS WKWebView findString not selecting and scrolling - ios

Since iOS 14 WebKit supports findString, but there is no documentation whatsoever yet.
However on the WWDC Sessions Discover WKWebView enhancements they mention that is a basic functionality for "Find on Page", where you can find a string and the WebView will select it and scroll to center it.
It seems very easy to use and to be finding the string as I get a result of matchFound true, but there is no selection and there is no scrolling. Maybe I'm missing something?
This is the code I have tried:
let webView = WKWebView()
// ...
// after loading a website with the desired string on it.
// ...
webView.find("hello world") { result in
print(result.matchFound) // true
}

Update for iOS 16
On iOS 16 we have the new UIFindInteraction API and now it is possible and very easy to do a Find on Page feature and search for a string.
myWebView.isFindInteractionEnabled = true
myWebView.findInteraction?.presentFindNavigator(showingReplace: false)
Important
This is not supported on macOS.
Original Answer
So far I was only able to make it 'kind of working' combining with a bit of JavaScript.
let webView = WKWebView()
webView.select(nil)
webView.find("hello world") { result in
guard result.matchFound else { return }
webView.evaluateJavaScript(
"window.getSelection().getRangeAt(0).getBoundingClientRect().top") { offset, _ in
guard let offset = offset as? CGFloat else { return }
webView.scrollView.scrollRectToVisible(
.init(x: 0,
y: offset + webView.scrollView.contentOffset.y,
width: 100,
height: 100), animated: true)
}
}
Description:
1.
webView.select(nil) to make it first responder.
This is important otherwise when the match is found it won't be selected.
2.
webView.find("my string")
3.
If match is found use JavaScript to get the offset to the selected text.
4.
When receiving the offset scroll to it.

Related

PDFKit - PdfView won't be RTL without usePageViewController()

I want to display PDF with horizontal, pages from right to left, page by page continuously, and users pinch one page will make all pages the same scale synchronized.
I wrote the codes as below:
if let pdfDocument = PDFDocument(url: pdfUrl) {
pdfView.document = pdfDocument
pdfView.displayMode = .singlePageContinuous
pdfView.displaysRTL = true
pdfView.autoScales = true
pdfView.autoresizesSubviews = true
pdfView.displayDirection = .horizontal
pdfView.displaysPageBreaks = true
pdfView.pageBreakMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
pdfView.maxScaleFactor = 4.0
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
}
But displaysRTL will not work, and default scale will not show whole single page but fit to the PDF page height (page right side will out of screen).
If I add
pdfView.usePageViewController(true)
PDF displayRTL works, and every page will fit screen size by default. BUT this will make PDF not displayed page by page, and zoom in scale will not the same with other pages.
The left page scale is not the same with current zoom in page on the right side. I need them all the same scale if user pinch a page.
Is there any way to fix this problem?
In addition, every page's top-left corner will display an icon, I don't know what is it mean and how to hide it..
I find a way that make it displayRTL manually.
Don't use usePageViewController(true), and just reordering the pages from the PDFDocument.
let doc = PDFDocument()
var getPage = pdfDocument.pageCount - 1
for i in 0..<pdfDocument.pageCount {
guard let pageRef = pdfDocument.page(at: getPage) else { fatalError() }
doc.insert(pageRef, at: i)
getPage -= 1
}
pdfView.document = doc
// Display PDF from last page if Right to Left
if (isPageRTL) {
pdfView.goToLastPage()
}
I found PDFView by Apple will get memory leak and crash if PDF is large in iOS 12 and iOS 13 (iOS 14 seems to be fine). So I have to find other way to display PDF that fit my need.

How to use UIPreviewParameters to specify a range of text as a highlighted preview for UIContextMenuInteraction without hiding the rest of the view?

Update for iOS 13.4 (March, 2020):
This also happens with UIPointerInteraction when hovering over the links.
I have a view that displays rich text and shows the iOS 13 context menu when the user long presses on a link. I want to be able to highlight just the link rather than the whole view when the user begins to long press.
To do this I provide a UITargetedPreview object that contains UIPreviewParameters with the CGRects of each line to be highlighted to the UIContextMenuInteractionDelegate of the view. This correctly highlights the link, but has the unwanted side effect of also hiding the rest of the view.
This image demonstrates the problem:
Notice that, while the link is highlighted correctly, the remainder of the view flashes in and out as the link is long pressed and then released.
Compare this to the behaviour in Apple's own Notes.app:
Notice that the rest of the view does not disappear when a link is long pressed. This also works as expected in Apple's other apps, too (e.g. Safari).
I provide UITargetedPreviews to the interaction delegate in the following way:
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard let range = configuration.identifier as? NSRange else { return nil }
let lineRects: [NSValue] = // calculate appropriate rects for the range of text
let parameters = UIPreviewParameters(textLineRects: lineRects)
return UITargetedPreview(view: /* the rich text view */, parameters: parameters)
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard let range = configuration.identifier as? NSRange else { return nil }
let lineRects: [NSValue] = // calculate appropriate rects for the range of text
let parameters = UIPreviewParameters(textLineRects: lineRects)
return UITargetedPreview(view: /* the rich text view */, parameters: parameters)
}
I can't find anything in what documentation there is for UITargetedPreview and UIPreviewParameters, so does anyone know how this can be done?
OK, I finally found out how to do it thanks to an implementation in WebKit. What I was doing wrong was not providing the targeted preview a UIPreviewTarget.
To highlight only a portion of a view you need to:
Provide UIPreviewParameters stating the portion of the view to show in the preview, and set the background colour to the same as the view you want to preview
Provide a UIPreviewTarget that establishes:
the whole view as the container of the animation
the centre of the portion being shown as the centre of the animation
Create a snapshot of the portion of the view you want to show and designate it as view of the UITargetedPreview.
In code this looks like:
let view: UIView = // the view with the context menu interaction
let highlightedRect: CGRect = // the rect of the highlighted portion to show
// 1
let previewParameters = UIPreviewParameters()
previewParameters.visiblePath = // path of highlighted portion of view to show, remember you can use UIPreviewParameters.init(textLineRects:) for text
previewParameters.backgroundColor = view.backgroundColor
// 2: Notice that we're passing the whole view in here
let previewTarget = UIPreviewTarget(container: view, center: CGPoint(x: highlightedRect.midX, y: highlightedRect.midY))
// 3: Notice that we're passing the snapshot view in here - not the whole view
let snapshot = view.resizableSnapshotView(from: highlightedRect, afterScreenUpdates: true, withCapInsets: .zero)!
return UITargetedPreview(view: snapshot, parameters: previewParameters, target: previewTarget)
Your solution is super helpful. I used hitTest to detect the highligted view and I am glad about the results, so just wanted to share.
- (UITargetedPreview *)contextMenuInteraction:(UIContextMenuInteraction *)interaction previewForHighlightingMenuWithConfiguration:(UIContextMenuConfiguration *)configuration {
UIView *view = interaction.view;
CGPoint location = [interaction locationInView:view];
// Detecting the hittable subview at the given location
UIView *subView = [view hitTest:location withEvent:nil];
if (subView) {
CGRect highlightedRect = subView.frame;
UIPreviewParameters *previewParameters = [UIPreviewParameters new];
previewParameters.backgroundColor = subView.backgroundColor;
previewParameters.visiblePath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, highlightedRect.size.width, highlightedRect.size.height)];
return [[UITargetedPreview alloc] initWithView:subView parameters:previewParameters];
}
return nil;
}

Algorithm for split string to fit size

I got a long string, let's call it Story , I fetch this Story from a Database so i don't know how long it is.
I want to display this story in view , But what if the Story is to long that doesn't fit in one view ?
I don't want to adjusts the font size Because it may be a very long story and make the font size smaller is not a good solution.
So i want to separate the Story to more than one view , By Passing the Story and get separated Story as array of String every item in the array can fit in one view .
This is the code , Maybe it give you a hint what i'm trying to do :
extension String {
/// - returns: The height that will fit Self
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)
return ceil(boundingBox.height)
}
#warning("it's so slow that i can't handle it in main thread , ( 21 second for 15 items in array )")
/// complition contains the separated string as array of string
func splitToFitSize(_ size : CGSize = UIScreen.main.bounds.size ,font : UIFont = UIFont.systemFont(ofSize: 17) , complition : #escaping (([String]) -> Void) ) {
DispatchQueue.global(qos: .background).async {
// contents contains all the words as Array of String
var contents = self.components(separatedBy: .whitespaces)
var values : [String] = []
for content in contents {
// if one word can't fit the size -> remove it , which it's not good, but i don't know what to do with it
guard content.isContentFit(size: size , font : font) else {contents.removeFirst(); continue;}
if values.count > 0 {
for (i , value) in values.enumerated() {
var newValue = value
newValue += " \(content)"
if newValue.isContentFit(size: size, font: font) {
values[i] = newValue
contents.removeFirst()
break;
}else if i == values.count - 1 {
values.append(content)
contents.removeFirst()
break;
}
}
}else {
values.append(content)
contents.removeFirst()
}
}
complition(values)
}
}
/// - returns: if Self can fit the passing size
private func isContentFit(size : CGSize, font : UIFont) -> Bool{
return self.height(withConstrainedWidth: size.width, font: font) < size.height
}
}
This code is working , But it take so long , if i want to split a Story to 15 views it take 20 or more second.
I'm not good in algorithms so i need a batter or a hint to make it execute quicker.
Just Hint in Any programming language, i be very thankful .
I have worked with this some times, for example for implementing an e-book reader, where you want the pages to flow to the right.
The code you posted is essentially correct, but it is slow because it has to rerender everything word-for-word as you measure it's size (assuming that your function isContentFit() is taking up most of your time). If you want to optimize this, you have to make a rough guess at the size of each letter and get an estimate on how many of your words can fit on one page before beginning to render and measure. You could also delay rendering/measuring the forthcoming pages until just before they need to display.
Another problem could also be the way you split up the string into words and then concatenate them back one by one. This can also be slow and time consuming if the string is large. Here it would also benefit to just search through the original string and counting letters and spaces, and then use string slicing when you know where to cut the string into pages.
Another solution I have participated in using successfully multiple times, is using a webview and styling your text so it will display in columns, for example by using styling like in this example: https://www.w3schools.com/cssref/tryit.asp?filename=trycss3_column-width
The web rendering engine does something like your code does, but blazingly fast.
On iOS, you should use the css property -webkit-column-width (since webviews on iOS is essentially rendered like in Safari) and set -webkit-column-width to the width of your webview, and then render the entire text into the webview. The result will then be that you see one 'page' at a time, and can scroll to the right to see the next page.
If you want to use a page controller or some other scrolling control on top of this, you have to inject some css/javascript magic, which is not easy I must admit, but it can be done.
Here is an example html string to display in the webview. Just insert your entire story string into the div:
<html>
<head>
<style>
.multicolumn {
-webkit-column-width: 500px; /* set this to the width of your webview */
height: 300px; /* set this to the height of your webview */
overflow-x: scroll;
}
</style>
</head>
<body>
<div class="multicolumn">
Insert long long text here...
</div>
</body>
</html>

Can't set cursor position in UITextView

I'm working on an app that is highly dependent on UITextView. My ideal behavior is when a user quickly presses a double space, the text cursor will jump ahead 5 spaces to make space for a UIButton. I've implemented the following code, but the cursor doesn't seem to jump even though all the other actions in the "quickSpace" area happen. Anyone know what I am doing wrong?
UPDATE: I think the problem is that I am trying to jump the cursor to a point outside of the range that currently exists. In other words, someone is entering new text into the UITextView and the range is set to wherever the cursor happens to be. Is that the end of the range? Is it possible to send a cursor outside of the range that currently exists?
if(text==" "){
let now=Date()
if let last=previousSpaceTimestamp
{
if now.timeIntervalSince(last)<0.3
{
//mainTextBox.text=textView.text.appending("hello")
isQuickSpace=true
var checkOffButton: UIButton=createCheckButton()
checkOffButton.backgroundColor=UIColor.green
checkOffButton.frame.size=CGSize(width: 20, height: 20)
//checkOffButton.frame.offsetBy(dx: 0, dy: CGFloat(buttonYPos))
mainTextBox.addSubview(checkOffButton)
buttonYPos=buttonYPos+15
if let currentRange=mainTextBox.selectedTextRange
{
if let newPosition=mainTextBox.position(from: currentRange.start,
offset: 200)
{
mainTextBox.selectedTextRange=mainTextBox.textRange(from: newPosition,
to: newPosition)
}
}
return false
}
}
previousSpaceTimestamp=now
}
else
{
previousSpaceTimestamp=nil
}
return true
}
Set the TextView Range Like this in Main Queue.
DispatchQueue.main.async {
mainTextBox.isEditable = true
mainTextBox.selectedRange = NSMakeRange(2, 0)
}

Diagonal swipe on web view populated by html script buggy after zoom

I have a web view that display a table much larger than the screen. When I try to scroll it diagonally AFTER zooming - both in or out, it usually only scrolls in a single direction instead of diagonally. Is this behaviour due to web view's scrollview or could I have made mistake in my codes?
This is how I populate my web view:
webViewContent.scrollView.bounces = false
webViewContent.scrollView.bouncesZoom = false
webViewContent.scrollView.delegate = self
webViewContent.scalesPageToFit = true
var htmlString = "<html><head>... ... a really long string that creates a table"
webViewContent.loadHTMLString(htmlString, baseURL: nil)
Please do tell me if the htmlString might affect, I did not include it because it is really long.
I also tried to synchronise the view with a header row called webViewTitle which I populate using similar codes but only one row. The synchronise codes are like:
func scrollViewDidZoom(_ scrollView: UIScrollView) {
if(scrollView != webViewTitle.scrollView){
var zoomPadding:CGFloat = 0.0
if((currentZoomScale*webViewContent.scrollView.zoomScale) < 1){
zoomPadding = 0.5*(-acos(currentZoomScale*webViewContent.scrollView.zoomScale)*180.0/CGFloat.pi)
}else{
zoomPadding = 0.5*acos(2-(currentZoomScale*webViewContent.scrollView.zoomScale))*180.0/CGFloat.pi
}
webViewTitle.scrollView.zoom(to: CGRect(x: webViewContent.scrollView.contentOffset.x,
y: (355*currentZoomScale*webViewContent.scrollView.zoomScale) + zoomPadding,
width: webViewTitle.scrollView.bounds.width/currentZoomScale/webViewContent.scrollView.zoomScale,
height: webViewTitle.scrollView.bounds.height/currentZoomScale/webViewContent.scrollView.zoomScale),
animated: false)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var zoomPadding:CGFloat = 0.0
if((currentZoomScale*webViewContent.scrollView.zoomScale) < 1){
zoomPadding = 0.5*(-acos(currentZoomScale*webViewContent.scrollView.zoomScale)*180.0/CGFloat.pi)
}else{
zoomPadding = 0.5*acos(2-(currentZoomScale*webViewContent.scrollView.zoomScale))*180.0/CGFloat.pi
}
if(scrollView == webViewTitle.scrollView){
webViewTitle.scrollView.contentOffset.y = (355*currentZoomScale*webViewContent.scrollView.zoomScale) + zoomPadding
webViewContent.scrollView.contentOffset.x = webViewTitle.scrollView.contentOffset.x
}else{
webViewTitle.scrollView.contentOffset.y = (355*currentZoomScale*webViewContent.scrollView.zoomScale) + zoomPadding
webViewTitle.scrollView.contentOffset.x = webViewContent.scrollView.contentOffset.x
}
}
Could any of these caused the diagonal scrolling to become buggy?
Could any of these caused the diagonal scrolling to become buggy?
Yes
From the provided code, you are implementing the UIScrollViewDelegate for the UIWebView, but in both shown implementations, you are not calling super. This means that the scrollView is not going to have it's standard behavior, and instead strictly use your code for it's scrollViewDidScroll and scrollViewDidZoom behavior.
While this question doesn't necessarily provide a lot of clarity on exactly what is going on, I believe it to be the same issue you're facing.
In summary: At a minimum, call super for each delegate implementation of the UIScrollViewDelegate, or if you don't need any custom scroll behavior then remove the delegate altogether.

Resources