Detect scroll in Webview with Swift - ios

I am using SwiftUI, so I have a wrapper around the Webview, like this:
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: String
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
webView.evaluateJavaScript("navigator.userAgent") { (result, error) in
print(result as! String)
}
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
let request = URLRequest(url: URL(string:url)!)
uiView.load(request)
}
}
How can I execute a function/method whenever the user makes scroll to the website?

To get scroll position updates you need to create coordinator and override makeCoordinator() method and return instance of your coordinator. In makeUIView(_:) method just assign scrollview delegate to context.coordinator (context object in provided in arguments of makeUIView method)
Pass binding from view to coordinator and coordinator is responsible to update that binding. Here is code for that
struct WebView: UIViewRepresentable {
var url: String
#Binding var contentOffset: CGPoint
init(url: String, contentOffset: Binding<CGPoint>) {
self.url = url
_contentOffset = contentOffset
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
webView.scrollView.delegate = context.coordinator // assign delegation
webView.evaluateJavaScript("navigator.userAgent") { (result, error) in
print(result as! String)
}
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
let request = URLRequest(url: URL(string:url)!)
uiView.load(request)
}
func makeCoordinator() -> Coordinator {
.init(contentOffset: $contentOffset) // create coordinator for delegation
}
class Coordinator: NSObject, UIScrollViewDelegate {
#Binding var contentOffset: CGPoint
init(contentOffset: Binding<CGPoint>) {
_contentOffset = contentOffset
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
contentOffset = scrollView.contentOffset
}
}
}

WebView with UIScrollViewDelegate support.
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: URL
var scrollViewDelegate: UIScrollViewDelegate?
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.scrollView.delegate = scrollViewDelegate
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}}
Observable ScrollViewDetector
import UIKit.UIScrollView
final class ScrollViewDetector: NSObject, ObservableObject, UIScrollViewDelegate {
#Published var isScrolledEnd = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentLoaded = scrollView.contentSize.height > 1000
let endOfContentReached = scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.size.height < 100
if contentLoaded && endOfContentReached {
isScrolledEnd = true
}
}}
Agreement view with sticky footer button.
import SwiftUI
struct AgreementView: View {
#StateObject var scrollViewDetector = ScrollViewDetector()
var body: some View {
GeometryReader { geometry in
ZStack {
WebView(url: URL(string: "YOUR URL")!, scrollViewDelegate: scrollViewDetector)
// Footer Navigation Link
NavigationLink {
EmptyView()
} label: {
// Your Label
Button("Accept")
.padding()
.background(Color.white.opacity(!scrollViewDetector.isScrolledEnd ? 0.9 : 0.0))
}
.position(x: geometry.width(0.5), y: geometry.height(0.9))
.opacity(!scrollViewDetector.isScrolledEnd ? 0.6 : 1.0)
.disabled(!scrollViewDetector.isScrolledEnd ? true : false)
}
}
}}

Related

How to use VLCMediaPlayer from MobileVLCKit in SwiftUi [duplicate]

I would like to use VLCKit with SwiftUI.
why not AVPlayer ? because a some video formats are not supported.
Here is my code:
import SwiftUI
import UIKit
struct ContentView: View {
var body: some View {
NavigationView
{
VStack{
PlayerView(url: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")
}
}
}
}
struct PlayerView: UIViewRepresentable {
public var url: String
let mediaPlayer = VLCMediaPlayer()
func makeUIView(context: Context) -> UIView {
let controller = UIView()
mediaPlayer.drawable = controller
let uri = URL(string: self.url)
let media = VLCMedia(url: uri!)
mediaPlayer.media = media
mediaPlayer.play()
return controller
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
}
This works and load the video but I would like to add playback controls (Full Screen, Play/Pause, forward, etc.).
I didn't find any documentation about this can anyone help me a bit,
Thanks
I watched this video on youtube
https://www.youtube.com/watch?v=N_u9nsXNvn4
and this
How to show my AVPlayer in a VStack with SwiftUI
Here is my demo
import SwiftUI
struct VlcPlayerDemo: UIViewRepresentable{
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VlcPlayerDemo>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(frame: .zero)
}
}
class PlayerUIView: UIView, VLCMediaPlayerDelegate {
private let mediaPlayer = VLCMediaPlayer()
override init(frame: CGRect) {
super.init(frame: frame)
let url = URL(string: "rtsp://xxxxx")!//replace your resource here
mediaPlayer.media = VLCMedia(url: url)
mediaPlayer.delegate = self
mediaPlayer.drawable = self
mediaPlayer.play()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
struct VlcPlayerDemo_Previews: PreviewProvider {
static var previews: some View {
VlcPlayerDemo()
}
}

PDFKit .go(to: ) Method in SwiftUI

hello i want to display a PDF file at page 7 in my SwiftUI app.
I would also like to use the functions .go(to :) and .currentPage() outside the UIViewRepresentable.
import SwiftUI
import PDFKit
struct PDFKitView: View {
var pdf: PDF
var body: some View {
PDFKitRepresentedView(pdf)
}
}
struct PDFKitRepresentedView: UIViewRepresentable {
let pdf: PDF
init(_ pdf: PDF) {
self.pdf = pdf
}
func makeUIView(context: UIViewRepresentableContext<PDFKitRepresentedView>) -> PDFKitRepresentedView.UIViewType {
let pdfView = PDFView()
let document = PDFDocument(data: pdf.content!)
pdfView.document = document
pdfView.autoScales = true
print("pdfView.currentPage: \(String(describing: pdfView.currentPage))") // pdfView.currentPage: Optional(<PDFPage: 0x600002245420> page index 0)
print("pdfView: \(pdfView)") // pdfView: <PDFView: 0x7fb206526c20; frame = (0 0; 0 0); gestureRecognizers = <NSArray: 0x600002ef4150>; layer = <CALayer: 0x60000202ddc0>>
if let myPage = document?.page(at: 7) {
pdfView.go(to: myPage)
}
print("pdfView.currentPage: \(String(describing: pdfView.currentPage))") // pdfView.currentPage: Optional(<PDFPage: 0x6000022455c0> page index 7)
print("pdfView: \(pdfView)") // pdfView: <PDFView: 0x7fb206526c20; frame = (0 0; 0 0); gestureRecognizers = <NSArray: 0x600002ef4150>; layer = <CALayer: 0x60000202ddc0>>
return pdfView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PDFKitRepresentedView>) {
// Update the view.
}
}
The PDF file is shown but at page 1 and not 7 :/
I'm still relatively new to it Swift, can anyone give me a hint?
this is how it works now
the problem was
PDFView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
and not
PDFView()
here the complete code
import SwiftUI
import PDFKit
struct PDFKitView: View {
#State var pdf: PDF
#Binding var pageIndex: Int
var body: some View {
VStack{
PDFPreviewController(pdfX: $pdf , pageIndex: $pageIndex )
}
}
}
class PDFPreviewViewConroller: UIViewController {
public var pdfView: PDFView!
override func loadView() {
pdfView = PDFView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
self.view = pdfView
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
struct PDFPreviewController: UIViewControllerRepresentable {
#Binding var pdf: PDF
#Binding var pageIndex: Int
init(pdfX: Binding<PDF>, pageIndex: Binding<Int>) {
_pdf = pdfX
_pageIndex = pageIndex
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PDFPreviewController>) -> PDFPreviewViewConroller {
return PDFPreviewViewConroller()
}
func updateUIViewController(_ uiViewController: PDFPreviewViewConroller, context: UIViewControllerRepresentableContext<PDFPreviewController>) {
uiViewController.pdfView.document = PDFDocument(data: pdf.content!)
if let myPage = uiViewController.pdfView.document?.page(at: (pageIndex)) {
uiViewController.pdfView.go(to: myPage)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(pdf: $pdf, pageIndex: $pageIndex)
}
class Coordinator: NSObject {
#Binding var pageIndex: Int
#Binding var pdf: PDF
init(pdf: Binding<PDF>, pageIndex: Binding<Int>) {
_pageIndex = pageIndex
_pdf = pdf
}
}
}

Using PKToolPicker without PKCanvasView

I am trying to use PKToolPicker from PencilKit (iOS/Swift) from a custom view (which is NOT PKCanvasView). The custom view conforms to PKToolPickerObserver. Everything works fine during compile time but I never get to see the PKToolPicker! If I replace my custom view with PKCanvasView, everything works fine!
I am doing this in SwiftUI with UIViewRepresentable (thus First Responder seems a mystery!).
Here is the SwiftUI view in question:
struct PencilKitView: UIViewRepresentable {
typealias UIViewType = myView
let coordinator = Coordinator()
class Coordinator: NSObject, PKToolPickerObserver {
func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) {
// some code
}
func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
// some code
}
}
func makeCoordinator() -> PencilKitView.Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> myView {
let canvasView = myView()
canvasView.isOpaque = false
canvasView.backgroundColor = UIColor.clear
canvasView.becomeFirstResponder()
if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first,
let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.addObserver(canvasView)
toolPicker.addObserver(coordinator)
toolPicker.setVisible(true, forFirstResponder: canvasView)
}
return canvasView
}
func updateUIView(_ uiView: myView, context: Context) {
}
}
If I replace myView with PKCanvasView above, the PKToolPicker will be seen.
For the sake of completeness, here is the MyView stub:
class myView: UIScrollView, PKToolPickerObserver {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
public func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
/// some code
}
public func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) {
/// some code
}
public func toolPickerIsRulerActiveDidChange(_ toolPicker: PKToolPicker) {
/// some code
}
public func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) {
/// some code
}
}
Anyone has succeeded in doing this? Is there some undocumented requirement for adopting PKToolPicker?
Here is simplest demo to show PKToolPicker for any custom UIView in SwiftUI.
Tested with Xcode 11.4 / iOS 13.4
struct ToolPickerDemo: View {
#State private var showPicker = false
var body: some View {
Button("Picker") { self.showPicker.toggle() }
.background(ToolPickerHelper(isActive: $showPicker))
}
}
class PickerHelperView: UIView {
override var canBecomeFirstResponder: Bool { true }
}
struct ToolPickerHelper: UIViewRepresentable {
#Binding var isActive: Bool
func makeUIView(context: Context) -> PickerHelperView {
PickerHelperView()
}
func updateUIView(_ uiView: PickerHelperView, context: Context) {
guard let window = uiView.window else { return }
let picker = PKToolPicker.shared(for: window)
picker?.setVisible(isActive, forFirstResponder: uiView)
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}

How to get notified in SwiftUI when UIKit UIViewController viewDidLoad finished

I have a UIViewController working fine in SwiftUI with the below code.
import Foundation
import SwiftUI
final class RTCVideoViewController: UIViewController {
var previewView: RTCEAGLVideoView!
var videoTrack: RTCVideoTrack!
override func viewDidLoad() {
previewView = RTCEAGLVideoView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
view.addSubview(previewView)
do {
videoTrack = try MediaCapturer.shared.createVideoTrack(videoView: previewView)
} catch {
print(error)
}
}
}
extension RTCVideoViewController : UIViewControllerRepresentable{
public typealias UIViewControllerType = RTCVideoViewController
public func makeUIViewController(context: UIViewControllerRepresentableContext<RTCVideoViewController>) -> RTCVideoViewController {
return RTCVideoViewController()
}
public func updateUIViewController(_ uiViewController: RTCVideoViewController, context: UIViewControllerRepresentableContext<RTCVideoViewController>) {
}
}
In SwiftUI view.
import Combine
import SwiftUI
struct LiveView: View {
#ObservedObject var viewModel: LiveViewModel
init(viewModel: LiveViewModel) {
self.viewModel = viewModel
}
var body: some View {
return ZStack {
RTCVideoViewController()
.edgesIgnoringSafeArea(.top)
}
}
}
So far everything works fine but I need the RTCVideoTrack when created to pass it to the LiveViewModel.
Any ideas what pattern I could implement to get notified when viewDidLoad finished or RTCVideoTrack passed to the LiveViewModel?
I changed a bit the code, it was close but simplified more.
import Foundation
import SwiftUI
struct RTCVideoViewController : UIViewControllerRepresentable {
var viewModel: LiveViewModel
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ viewController: UIViewController, context: Context) {
print("HostBroadcastViewController:updateUIViewController")
let previewView = RTCEAGLVideoView(frame: CGRect(x:0, y:0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
previewView.contentMode = UIView.ContentMode.scaleAspectFit
viewController.view.addSubview(previewView)
do {
let videoTrack = try MediaCapturer.shared.createVideoTrack(videoView: previewView)
self.viewModel.videoTrack = videoTrack
}
catch {
print("ERROR getting RTCVideoTrack")
}
}
}
And in SwiftUI init the RTCVideoViewController with RTCVideoViewController(viewModel: self.viewModel) where ViewModel passed in view from parent.

Scroll info from SwiftUI List - UIViewRepresentable UIScrollView List bindings not always working with SwiftUI

I'm trying to observe/get some of SwiftUI's List scrolling attributes by wrapping/injection of UIScrollView using UIViewRepresentable.
I'm getting inconsistent behavior with bindings.
Pressing the buttons and changing values depending on if the button is in the parent vs child view has different results.
The bindings from my ObservableObject ScrollInfo class and the UIViewRepresentable start fine, but then break, unless the whole screen is refreshed and makeUIView runs again (like changing to a different tab).
Is there a way to force the UIViewRepresentable to run makeUIView again on a binding update? Or something that will fix this?
I'd like for isScrolling values to be updated and working all the time.
I set up a test to change the colors of the circles to red if the user is dragging the scrollview down. It works initially but stops if I update a value from the ObservableObject in the parent view.
Screenshots of Test from code below
Bindings keep working with bottom button press (updating ObservableObject) in child view
Bindings break with top button press (updating ObservableObject) in parent view
// Parent View
import SwiftUI
struct ContentView: View {
#ObservedObject var scrollInfo:ScrollInfo = ScrollInfo()
var body: some View {
VStack
{
Button(action:{
self.scrollInfo.contentLoaded = true;
})
{
Text("REFRESH")
}
TestView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// Child View
import SwiftUI
struct TestView: View {
#State var test1:String = "Test1"
#ObservedObject var scrollInfo:ScrollInfo = ScrollInfo()
var body: some View {
VStack
{
Button(action:{
self.scrollInfo.contentLoaded.toggle()
}) {
Text("REFRESH")
}
List{
VStack {
VStack{
Text(String( self.scrollInfo.contentLoaded))
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.scrollInfo.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.scrollInfo.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.scrollInfo.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.scrollInfo.isScrolling ? .red : .green)
}
} .padding(.bottom,620).padding(.top,20)
.background(
ListScrollingHelper(scrollInfo: self.scrollInfo)// injection
)
}.padding(.top,4).onAppear(perform:{
})
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
// ScrollInfo Class
class ScrollInfo: ObservableObject {
#Published var isScrolling:Bool = false
#Published var text:String = "Blank"
#Published var contentLoaded:Bool = false
init()
{
print( "scrollInfo init")
}
deinit {
print("scrollInfo denit")
}
}
// UIViewRepresentable
final class ListScrollingHelper: UIViewRepresentable {
var scrollInfo:ScrollInfo
#Published var scrollView: UIScrollView?
init( scrollInfo:ScrollInfo) {
print("init UIViewRepresentable listscrolling helper")
self.scrollInfo = scrollInfo
}
func makeUIView(context: Context) -> UIView {
//self.uiScrollView.delegate = context.coordinator
print("makeview")
return UIView()
//return self.uiScrollView // managed by SwiftUI, no overloads
}
func catchScrollView(for view: UIView) {
print("checking for scrollview")
if nil == scrollView {
scrollView = view.enclosingScrollView()
if(scrollView != nil)
{
print("scrollview found")
}
}
}
func updateUIView(_ uiView: UIView, context: Context) {
catchScrollView(for: uiView)
if(scrollView != nil)
{
scrollView!.delegate = context.coordinator
}
print("updatingUIView")
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject,UIScrollViewDelegate {
var parent: ListScrollingHelper
init(_ listScrollingHelper: ListScrollingHelper) {
self.parent = listScrollingHelper
print("init coordinator")
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// DispatchQueue.main.async {
if(!self.parent.scrollInfo.isScrolling)
{
self.parent.scrollInfo.isScrolling = true
//self.parent.scrollInfo.text = "scroll"
// }
}
print("start scroll")
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if(self.parent.scrollInfo.isScrolling && !decelerate)
{
self.parent.scrollInfo.isScrolling = false
}
print("end scroll")
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if(self.parent.scrollInfo.isScrolling)
{
self.parent.scrollInfo.isScrolling = false
}
print("end scroll")
}
deinit
{
print("deinit coordinator")
}
}
deinit {
print("deinit UIViewRepresentable listscrolling helper")
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
EDIT - WORKAROUND
I wasn't able to get it working with the ObservableObject or EnvironmentObject, but I was able to get it working with #State and #Binding, although it's a limited amount of info passed back. (ScrollInfo is still there only to use to testing changing a parent ObservableObject)
Hope this helps someone else!
import SwiftUI
struct TestView: View {
#State var isScrolling:Bool = false;
var body: some View {
VStack
{
Button(action:{
self.scrollInfo.contentLoaded.toggle()
}) {
Text("REFRESH")
}
List{
VStack {
VStack{
Text("isScrolling")
Text(String(self.isScrolling))
Circle().frame(width:50,height:50).foregroundColor(self.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.isScrolling ? .red : .green)
}
VStack{
Text(self.scrollInfo.text)
Text(self.test1)
Circle().frame(width:50,height:50).foregroundColor(self.isScrolling ? .red : .green)
}
} .padding(.bottom,620).padding(.top,20).background( ListScrollingHelper(isScrolling: self.$isScrolling))
}.padding(.top,4)
}
}
}
extension UIView {
func enclosingScrollView() -> UIScrollView? {
var next: UIView? = self
repeat {
next = next?.superview
if let scrollview = next as? UIScrollView {
return scrollview
}
} while next != nil
return nil
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
final class ListScrollingHelper: UIViewRepresentable {
#Binding var isScrolling:Bool
private var scrollView: UIScrollView?
init(isScrolling:Binding<Bool>)
{
self._isScrolling = isScrolling
}
func makeUIView(context: Context) -> UIView {
return UIView()
}
func catchScrollView(for view: UIView) {
if nil == scrollView {
scrollView = view.enclosingScrollView()
}
}
func updateUIView(_ uiView: UIView, context: Context) {
catchScrollView(for: uiView)
if(scrollView != nil)
{
scrollView!.delegate = context.coordinator
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject,UIScrollViewDelegate {
var parent: ListScrollingHelper
init(_ listScrollingHelper: ListScrollingHelper) {
self.parent = listScrollingHelper
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if(!self.parent.isScrolling)
{
self.parent.isScrolling = true
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if(self.parent.isScrolling && !decelerate)
{
self.parent.isScrolling = false
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if(self.parent.isScrolling)
{
self.parent.isScrolling = false
}
}
deinit
{
}
}
deinit {
}
}
class ScrollInfo: ObservableObject {
#Published var isScrolling:Bool = false
#Published var text:String = "Blank"
#Published var contentLoaded:Bool = false
}

Resources