SwiftUI ScrollView not properly setting content offset when nested in UIViewRepresentable - swift

I'm trying to introspect the UIScrollView under the hood of a SwiftUI ScrollView and that's all fine and dandy.
But I noticed that there were some issues with spacing and animation when scrolling going the pure representable approach so instead I'm trying to leverage a few properties of the underlying UIScrollView via a few bindings that are updates as part of the scrollview delegate.
That's not the main concern, the main issue I'm having even if I'm not doing any bindings at all but a bare bones approach of using the ScrollView in a UIViewRepresentable context is that it behaves differently.
public struct PerfectScrollView<Content: View>: UIViewRepresentable {
private let content: Content
public init(#ViewBuilder content: () -> Content) {
self.content = content()
}
public func updateUIView(_ uiView: UIViewType, context: Context) {
}
public func makeUIView(context: Context) -> UIView {
UIHostingController(rootView:
ScrollView {
content
}
).view
}
}
// Usage
PerfectScrollView {
ForEach(0..<100) { Text("Hello \($0)") }
}
// vs
ScrollView {
ForEach(0..<100) { Text("Hello \($0)") }
}
The result of PerfectScrollView renders this where I'm centered in the middle of the scrollview's content.
But the normal ScrollView (not setup via UIViewRepresentable protocol) renders an appropriate scrollview.
Any ideas about what actually is happening? From interacting with he PerfectScrollView it's like I've reached the end/bounds of the view and trying to scroll results in the rubber band style resistance animation scrolling up or down.
Any help/feedback would be greatly appreciated :)

The solution was to use UIViewControllerRepresentable protocol since i was using ScrollView in a UIHostingController context.

Related

how to better implement reverse infinite scroll for conversation like screens

In a chat-like app, the newest messages are shown at the bottom of the list. As you scroll up, the older messages are loaded and displayed, similar to the endless scrolling we’re accustomed to.
There are several options available to match this use case:
The first option is to use a List and invert it 180 degrees.
Another option is to use a ScrollView with a LazyVStack and invert them, similar to the List approach.
Another approach would be to cheat and fall back to the well-tested UIKit solutions.
Since SwiftUI is the future, I decided against the UIKit options (unless essential). and went for the ScrollView/LazyVStack option.
The problem is that when the items are prepended to the list, the ScrollView start-position (offset) is always the first item of the prepended list.
I cannot think of a non-hacky solution to force the ScrollView to stick with its initial offset (help is appreciated).
attaching an example code of both my reversed ScrollView and main ChatView screen.
struct ReversedScrollView<Content: View>: View {
var content: Content
init(#ViewBuilder builder: () -> Content) {
self.axis = axis
self.content = builder()
}
var body: some View {
GeometryReader { proxy in
ScrollView(axis) {
VStack {
Spacer()
content
}
.frame(
minWidth: minWidth(in: proxy, for: axis),
minHeight: minHeight(in: proxy, for: axis)
)
}
}
}
}
public struct ChatView: View {
public var body: some View {
GeometryReader { geometry in
messagesView(in: geometry)
}
}
#ViewBuilder private func messagesView(in geometry: GeometryProxy) -> some View {
ReversedScrollView {
ScrollViewReader { proxy in
LazyVStack {
ForEach(messages) { message in
ChatMessageCellContainer(
message: message,
size: geometry.size,
)
.id(message.id)
}
}
}
}
}
}

AppKit's view bridged to swiftui causes view to stretch outside view/window frame

I'm having layout issues with mixed AppKit(bridged to SwiftUI)/SwiftUI views. I have simple view setup containing 2 rows where row is defined as: text view + text field from app kit bridged to swiftui.
I need my text views left of the text field to be aligned to the right, a.k.a it looks like they have trailing alignment. But since my view hierarchy defines views as rows and not as VStacks, I need customized alignment guide.
It all works fine when I replace bridged view with some SwiftUI's native view i.e Text().
Any ideas what may be causing this and how to fix it? I've tried to play around with setting content compression & hugging priorities on AppKit view or layout priorities for both SwiftUI + AppKit to SwiftUI views, without success.
Setting some specific frame size may have achieve somehow what I want, but since my texts on the left side are localized and may be arbitrarily long (and I basically want them to show them whole without truncating) and whole view may be resized horizontally too, it's not a good solution.
Here's very simplified code, that does illustrate the issue:
import AppKit
import Combine
import SwiftUI
extension HorizontalAlignment {
struct TrailingTextContent: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let trailingTextContent = HorizontalAlignment(TrailingTextContent.self)
}
struct TextField: NSViewRepresentable {
#Binding private var value: String
init(value: Binding<String>) {
self._value = value
}
func makeNSView(context: Context) -> NSTextField {
let textField = NSTextField(frame: .zero)
return textField
}
func updateNSView(_ nsView: NSTextField, context: Context) {}
}
struct DetailsView: View {
var body: some View {
VStack(alignment: .trailingTextContent, spacing: 8) {
HStack(alignment: .center, spacing: 0) {
Text("Long")
.padding(.trailing, 8)
.alignmentGuide(.trailingTextContent) { d in d[HorizontalAlignment.trailing] }
TextField(value: .constant("asdasdasdasdasdasdada"))
}
HStack(alignment: .center, spacing: 0) {
Text("A bit longer text")
.padding(.trailing, 8)
.alignmentGuide(.trailingTextContent) { d in d[HorizontalAlignment.trailing] }
TextField(value: .constant("asdasdasdasdasdasdada"))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
DetailsView()
}
}
This is what it looks like:
and this is sort of how I want it to look like (+ text fields should be both same length) - I've applied some padding to whole view

SwiftUI TabView with shared content view across multiple tabs

I am building an application that is based around WKWebView but uses some native elements. I am looking to add tabs to it and I reached for TabView, but the problem I am facing is that for each tab, I need to have a new instance of WKWebView:
TabView(selection: $tab) {
ForEach(tabs, id: \.0) { (tag, name, icon) in
MyWebView()
.tabItem {
Image(systemName: icon)
Text(name)
}
.tag(tag)
}
}
.onChange(of: tab) { tab in
// postMessage to the WKWebView
}
This makes it so that each tab has its own WKWebView instance and whenever I tap on a tab, it sends a message to the associated web view.
I would however prefer to have a single WKWebView and somehow wire up the TabView such that it would show that same single WKWebView for each tab so that I could just send a message to the sole WKWebView instructing it to switch to the web view appropriate for the current tab.
Is this possible? Can I perhaps create a row of tabs like this without using TabView so that I could stack the sole WKWebView on top of this row?
It is possible to have single static instance of WKWebView in representable, everything else depends on app logic
struct MyWebView: UIViewRepresentable {
static let nativeWebView = WKWebView() // << here !!
func makeUIView(context: Context) -> some UIView {
Self.nativeWebView // << here !!
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}

How does the modifier .navigationTitle in SwiftUI work?

I've known the .navigationTitle is the extension function of View, but how to explain the following examples?
var body: some View {
NavigationView{
ScrollView{
ForEach(1..<100){ item in
Text("Hello, \(item)!")
.navigationTitle("Test\(item)")
}
}
}
.navigationTitle("title in navigation")
}
The result show that only the modifier of first widget inside NavigationView effected.
code results
I think the best choice is: modifier .navigationTitle is effective in NavigationView instead of the first widget inside NavigationView.
iOS will show the first innermost .navigationTitle.
Your outer title will never show, as it cannot be attached to "NavigationViewitself.
From the docs:
A view’s navigation title is used to visually display the current navigation state of an interface. On iOS and watchOS, when a view is navigated to inside of a navigation view, that view’s title is displayed in the navigation bar. On iPadOS, the primary destination’s navigation title is reflected as the window’s title in the App Switcher. Similarly on macOS, the primary destination’s title is used as the window title in the titlebar, Windows menu and Mission Control.
It doesn't really matter if .navigationTitle is inside a NavigationView or not. What .navigationTitle does is finds the UIView that the View is being displayed in, then searches for the UIViewController containing that UIView and it sets its navigationItem.title. That fact that only the first title param is used is probably some implementation detail, e.g. if this value is already set don't set it again, because obviously searching the UIView and UIViewController hierarchy is an expensive operation they would want to avoid.
You can verify this by implementing a UINavigationController in SwiftUI using UIViewControllerRepresentable. Then when you put a UIHostingController in the stack, if the SwiftUI View uses .navigationTitle then it still works. e.g.
struct NavigationControllerTestView: View {
var body: some View {
MyNavigation {
Text("Test Text")
.navigationTitle("Test Title") // works despite no NavigationView
.toolbar {
EditButton()
}
}
}
}
struct MyNavigation<Content: View>: UIViewControllerRepresentable {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> UINavigationController {
let hc = UIHostingController(rootView: content)
hc.rootView = content
let vc = UINavigationController(rootViewController: hc)
return vc
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
}
typealias UIViewControllerType = UINavigationController
}

How to disable vertical bounce in SwiftUI on a single ScrollView

I have a half modal view coming from the bottom of the screen with a scrollView, and when there's isn't enough content to scroll I want the drag gesture on the internal scrollView to apply to the modal and expand it or collapse it.
I tried using:
init() {
UIScrollView.appearance().bounces = false
}
And it works fine but this disables the bouncing effect on all the scrollViews in my app.
Is there a way to apply this for a single ScrollView or at least a single View?
you can add this to a ViewModifier:
struct SomeModifier: ViewModifier {
init() {
UIScrollView.appearance().bounces = false
}
func body(content: Content) -> some View {
return content
}
}
ScrollView {
Text("Some scroll view")
}.modifier(SomeModifier()