Scroll to HTML element ID in VStack of WKWebView views - swift

I have a ScrollView with a LazyVStack outputting items in ForEach.
I'm rendering HTML in the LazyVStack with the RichText library: https://github.com/NuPlay/RichText which is essentially a dynamically sized WKWebView for each item in the VStack.
#State var scrollTarget: Int?
//struct view
ScrollView {
ScrollViewReader { proxy in
LazyVStack {
ForEach(ficDetail.chapters.indices, id: \.self ) { i in
Text(self.ficDetail.chapters[i].post_title)
.id(self.ficDetail.chapters[i].id)
RichText(html: self.ficDetail.chapters[i].html)
.lineHeight(170)
.colorScheme(ColorScheme.auto)
.imageRadius(6)
}
}
.onChange(of: scrollTarget) { newValue in
if let target = newValue {
scrollTarget = nil // allows for future triggers
proxy.scrollTo(target)
}
}
}
}
As the above code shows, I'm using ScrollViewReader to auto scroll the ScrollView to the Text view with property: .id(self.ficDetail.chapters[i].id).
The issue is that the HTML I'm displaying inside each RichText view is very large, and I need to let users auto scroll to specific areas inside each RichText HTML view, particularly if they close the app and I want to restore the Scene.
The rendered HTML has <span id="{Int}"> ... </span> wrapping each sentence inside the HTML.
Is there anyway to bind proxy.scrollTo to the embedded <span> IDs to scroll the LazyVStack to that <span id="{Int}">?
If not, is there another method already in practice for this use case?
When I webView.evaluateJavaScript("document.getElementById('30').scrollIntoView();" this only scrolls the inner WKWebview and not the outer ScrollView, so the HTML content inside the view is getting cutoff as it's scrolling out of view.
I'm considering recording the scroll offset of the ScrollView and finding a way to restore the Scene and scroll back to that offset. Though seems messy as the HTML content + CSS can change the offset sizes.
Another option I considered is adding HiddenView slices with .id()s proportional to the size of each RichText view to act as a proxy to the WKWebViews, but that also seems hacky.

Related

How to make mouse scroll to scroll horizontally in swiftui?

I have a View built using SwiftUI that uses Scroll view horizontally.
var body: some View {
VStack {
// this should scroll horizontally when user uses the mouse scroller
HStack {
}
}
}
Currently user can do that by pressing shift button and scroll but I want to get rid of shift button
Goal: Mouse scroller should scroll horizontally without using shift button
You can see a tutorial here:
https://www.youtube.com/watch?v=-JjN2sRQXLc&t=1176s
With the final code here:
https://github.com/gahntpo/ItunesSearchApp/blob/main/ItunesSearchApp/view/album/SongsForAlbumListView.swift
In sum you will need a ScrollViewReader that gives you a closure with a proxy that could be called to scrollTo method.
You'll need to mart the destinations to scrollTo with an identifiable object or using .id modifier.
All explained in the video.

SwiftUI: Disable ScrollView scrolling to top if top of screen is touched

I have a UI which resides in a VStack{} with a custom topbar and a ScrollView. I have buttons on my top bar which do things like open user account details. I find that if I press some of these buttons the ScrollView autoscrolls to the top of the screen. Is there a way to stop that?
My code:
struct MyView: View {
var body: some View {
VStack(spacing:0) {
TopBarView()
ScrollView(.vertical) {
ForEach(0..<100, id: \.self) { index in
Text(String(index))
}
}
}
}
struct TopBarView: View {
var body: some View {
Text("This is a top bar")
}
}
}
If I touch the top of the screen above the scrollView's frame, the scrollView scrolls all the way to the top. Is there a way to disable that behavior?
This is not a SwiftUI or Xcode or ScrollView problem.
This is a built-in feature of iPhone.
When you tap the top-center edge of the screen, displayed contents will be scrolled up.
Try opening apps like Facebook, WhatsApp, then scroll and tap at the same position, you will see the displayed contents scroll up.
For UIScrollView, there's a scrollsToTop property you can disable:
let scrollview = UIScrollView()
scrollview.scrollsToTop = false
Nothing for SwiftUI's ScrollView as far as I can tell. You could use UIViewRepresentable to wrap UIScrollView for SwiftUI.

Pass through clicks/taps to underlying UIViewRepresentable

I have a UIViewRepresentable that I am using the blend a project created with UIViews and SwiftUI.
My views are full screen and my issue is that I cannot find a way to pass touches through the transparent UINavigationBar at the top of the screen to my underlying UIView. That means that the top 100 or so pixels don't respond to touch even though they're visible. I have seen this post and solution, but it does not work in my case. I believe it's because I'm using UIViewRepresentable.
For my content view I have:
struct ContentView: View {
let values: [String] = ViewType.allCases.map { "\($0.rawValue)" }
var body: some View {
NavigationView {
List {
ForEach(values, id: \.self) { value in
NavigationLink(value, destination: CustomView(viewName: value).ignoresSafeArea())
}
.navigationTitle("Nav Title")
}
}
.accentColor(.white)
}
}
struct CustomView: UIViewRepresentable {
let viewType: String
func makeUIView(context: Context) -> CustomView {
CustomView(view: viewType)
}
func updateUIView(_ view: CustomView, context: Context) {
view.draw(view.frame)
}
}
Here is an image of my view hierarchy that shows the UINavigationBar blocking the top 100 or so pixels of my underlying UIView and preventing touches from being registered by the view.
I know I can disable the navigation bar and that does work, but I do need the back button. Ideally, I'd like to understand how I can apply the solution I linked to above which will pass touches unless there is a UIControl in the way.
UPDATE
I tried Asperi's suggestion of using transparency and then disabling the navigation bar and it occurred to me to check the view debugger with it disabled.
It turns out that the back button is still there in the view hierarchy, but it is not visible in the app when the nav bar is disabled. If there were a way to keep the nav bar disabled but enable the button, that would be the ideal situation.

ScrollView nested in TabView reset position to start while changing tab

I'm experimenting with SwiftUI and I found a weird behaviour of ScrollView when nested in a TabView. If I swipe the ScrollView down and then do a slow gesture to swipe to the next tab, it sometimes reset the scrollView Position to the start. I'm not sure how to explain it well so here is a gif:
I'm trying to understand what cause this and if there is a way to avoid it.
Here is the code I use:
var body: some View {
TabView(selection: $selectedTab,
content: {
ForEach(coreData.pageList, id: \.self) { page in
if(coreData.pageList[page] == nil) {
ProgressView().onAppear(perform: {
loadPage(page: page)
})
} else {
ScrollView {
Text("START OF PAGE ------- -----Page Number: \(page)! This is a very short text made big to simulate scrolling. This is a very short text made big to simulate scrolling. ").font(.system(size: 90))
}
}
}
})
}
Am I missing something simple, is it a bug (with workaround), am I using these views wrong?
Ultimately I just want to have something that look like a page that you swipe right left or right but having the scroll position reset so fast would not be great user experience and doesn't look great
Edit: I haven't found any solution yet, but an interesting point: if I put the ProgressView inside the scrollView the scrollview does NOT reset position but the next tab will already be scrolled with the same amount as the previews page. This is equally "bad" if not worse :-(.
Try to put it into separated view (passing all needed parameters), so SwiftUI will see that view not changed and so not rerender it, ie. it should look like
if(coreData.pageList[page] == nil) {
ProgressView().onAppear(perform: {
loadPage(page: page)
})
} else {
DetailsView(page: page) // << ScrollView inside
}

How to dynamically adjust the number of items in a row within a vertically scrollable VStack in SwiftUI?

Input: I have a list of View where each View is a VStack of an Image and Text
Requirement: I want to display them vertical scrollable view. However, each row should have more than one of such Views (based on how many can fit). [Note that Views are not equal in size as the Text differs]. It should NOT be horizontally scrollable
Problem: How do I dynamically adjust the number of strings in a row based on how many can fit? See Image below as an example
Appreciate inputs and thanks in advance!
I would advice you to use LazyVGrid with adaptive type.
And here you no longer need to use List, just use the array of views
It's a new feature so it will work in ios14.0 and above
Struct MainView : View {
private let columnGrid = {
GridIten(.adaptive(minimum:50)) //here you can put minimum width size value unto you.
}
var body: some View {
LazyVGrid(columns: columnGrid) {
//your view with image will come here
}
}
}