Pass through clicks/taps to underlying UIViewRepresentable - swift

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.

Related

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.

SwiftUI - Display view on UIWindow

I'm trying to display a custom SwiftUI view similar to a Toast in Android.
My issue is that I would like to display this particular view above everything else, using the current UIWindow.
Currently, while working on static func displayToastAboveAll() located in my ToastView, this is how far i got
public struct ToastView: View {
static func displayToastAboveAll() {
let window = UIApplication.shared.windows.filter { $0.isKeyWindow }.first // window
let viewToShow = ToastView(my params) // my view to display
// This part I'm not sure of
let hostingController = UIHostingController(rootView: viewToShow)
window?.addSubview(hostingController.view)
}
public var body: some View {
// MyDesign
}
}
Any idea how should I use the window to put the ToastView at its proper place, and still being able to navigate within the app (and use the outlets) while having the view displayed ?
I managed to do what I wanted.
Basically, this code is working, but I had to remove some constraints from my SwiftUI view and add them with UIKit using the static func.
Also, I had to pass by a modifier (see below) and put ToastView init in private.
public struct ToastModifier: ViewModifier {
public func body(content: Content) -> some View {
content
}
}
extension View {
public func toast() -> some View {
ToastView.displayToastAboveAll()
return modifier(ToastModifier())
}
}
This is done to force the use of either modifier (SwiftUI, by doing .toast, just like you'd do .alert) or directly by calling the static func ToastView.displayToastAboveAll() (UIKit).
Indeed, I dont wont this Toast to be a part of the view, I want to trigger it like an alert.
Finally, special warning because passing ToastView into UIHostingViewController will mess with some of the animations.
I had to rewrite animations in UIKit in order to have a nice swipe & fade animation.

SwiftUI - Position an overlay relative to its anchor

I have a ZStack containing 2 views:
referenceContent - has some content and a Divider. This is the main content across the screen
popoverContent - is a conditional popup window that only takes up a tiny portion of the screen.
var body: some View {
ZStack {
referenceContent
if popoverCondition {
popoverContent
}
}
}
I want the popoverContent's top edge to line up with the bottom of referenceContent
Anyone know how to make this happen? Or is there just a much better way to view this popup window than I'm doing now? Thanks!
You can do this using the overlay(alignment:content:) modifier (previously overlay(_:alignment:)) in combination with custom alignment guides.
The basic idea is that you align the bottom of your reference view with the top of your popover view.
The annoying thing is that the overlay modifier only lets you specify one alignment guide (for the two views). So if you write stack1.overlay(alignment: .bottom) { stack2 } it will align the bottom of your reference with the bottom of your overlay. A quick way to overcome this is to overwrite the bottom alignment guide of your overlay and return the top instead.
referenceView
.overlay(alignment: .bottom) {
popoverContent
// overwrites bottom alignment of the popover with its top alignment guide.
.alignmentGuide(.bottom) {$0[.top]}
}
Overlay vs ZStack
You might ask: "why don't you use a ZStack instead of an overlay?". Well the difference between the two is that the ZStack will take the size of your popover into consideration when laying out your entire view (reference + popover). That is the opposite of what a popover should do. For a popover, the layout system should only take the size of your reference view into consideration and draw the popover on top of it (without affecting the layout of your reference). That is exactly what the overlay(...) modifier does.
Old API (prior to iOS 15, macOS 12)
In older versions of SwiftUI the arguments of the overlay modifier were in reverse order. So the code example for these older systems is:
referenceView
.overlay(
popoverContent.alignmentGuide(.bottom) {$0[.top]},
alignment: .bottom
)
Custom alignment guides
When you don't want to overwrite an existing alignment guide (because you need it somewhere else for example) you can also use a custom alignment guide. Here is a more generic example using a custom alignment guide named Alignment.TwoSided
extension View {
#available(iOS 15.0, *)
func overlay<Target: View>(align originAlignment: Alignment, to targetAlignment: Alignment, of target: Target) -> some View {
let hGuide = HorizontalAlignment(Alignment.TwoSided.self)
let vGuide = VerticalAlignment(Alignment.TwoSided.self)
return alignmentGuide(hGuide) {$0[originAlignment.horizontal]}
.alignmentGuide(vGuide) {$0[originAlignment.vertical]}
.overlay(alignment: Alignment(horizontal: hGuide, vertical: vGuide)) {
target
.alignmentGuide(hGuide) {$0[targetAlignment.horizontal]}
.alignmentGuide(vGuide) {$0[targetAlignment.vertical]}
}
}
}
extension Alignment {
enum TwoSided: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat { 0 }
}
}
You would use that like this:
referenceView
.overlay(align: .bottom, to: .top, of: popoverContent)

Why is makeUIView being called multiple times when within TabView?

My app has a TabView (PageTabViewStyle) and within each tab is a UIViewRepresentable. I have simplified the code to produce a minimum reproducible example below. The problem is that makeUIView is being called multiple times when it appears, and again multiple times when swiping through the pages.
FYI, this was not an issue in iOS 14.0 but is happening in iOS 14.2. I am using Xcode 12.2 Release Candidate at the moment.
import SwiftUI
struct MakeUIViewTest: View {
var body: some View {
TabView {
CustomViewRepresentable(color: .red, index: 0)
CustomViewRepresentable(color: .blue, index: 1)
CustomViewRepresentable(color: .gray, index: 2)
}
.tabViewStyle(PageTabViewStyle())
}
}
struct MakeUIViewTest_Previews: PreviewProvider {
static var previews: some View {
MakeUIViewTest()
}
}
struct CustomViewRepresentable: UIViewRepresentable {
var color: UIColor
var index: Int
func updateUIView(_ uiView: UIViewType, context: Context) {
}
func makeUIView(context: Context) -> UIView {
print("MAKING UI VIEW NOW. INDEX \(index)")
let view = UIView(frame: .zero)
view.backgroundColor = color
return view
}
}
When the screen appears, the console prints:
MAKING UI VIEW NOW. INDEX 0
MAKING UI VIEW NOW. INDEX 0
MAKING UI VIEW NOW. INDEX 0
MAKING UI VIEW NOW. INDEX 0
MAKING UI VIEW NOW. INDEX 0
And when I swipe to the right once, it prints:
MAKING UI VIEW NOW. INDEX 1
MAKING UI VIEW NOW. INDEX 2
MAKING UI VIEW NOW. INDEX 1
MAKING UI VIEW NOW. INDEX 1
MAKING UI VIEW NOW. INDEX 1
MAKING UI VIEW NOW. INDEX 1
MAKING UI VIEW NOW. INDEX 2
You must implement this method and use it to create your view object. Configure the view using your app’s current data and contents of the context parameter. The system calls this method only once, when it creates your view for the first time. For all subsequent updates, the system calls the updateUIView(_:context:) method. Hope this helps.
https://developer.apple.com/documentation/swiftui/uiviewrepresentable/makeuiview(context:)

Set segment equal width for SwiftUI Picker with SegmentedPickerStyle

Using the SegmentedPickerStyle style Picker could make the control looks like UISegmentedControl. But I wonder how to adjust the segment width in the picker. For examle, the picker in the image has a different width for text.
Is there a way to make the segments the same width in the SwiftUI?
Picker(selection: $store.utility.saliencyType, label: EmptyView()) {
ForEach(Store.Utility.SaliencyType.allCases, id: \.self) { saliencyType in
Text(saliencyType.text)
.tag(saliencyType)
}
}.pickerStyle(SegmentedPickerStyle())
...For examle, the picker in the image has a different width for text.
In case you arrive here seeking for iOS SwiftUI SegmentedPickerStyle solution... I've found the iOS SwiftUI .pickerStyle(SegmentedPickerStyle()) will conform to global UISegmentedControl.appearance() settings, so I've used the following to successfully apportion the width of each segment:
UISegmentedControl.appearance().apportionsSegmentWidthsByContent = true
This is particularly useful if, for example, you want to support Dynamic Type fonts in your app, which can otherwise cause segments with longer names to blow out and get truncated. [aside: I also use this trick to change the SwiftUI segmented picker's font size! see https://stackoverflow.com/a/71834578/3936065]
This is default macOS NSSegmetedControl behavirour
#property NSSegmentDistribution segmentDistribution API_AVAILABLE(macos(10.13));
// Defaults to NSSegmentDistributionFill on 10.13, older systems will continue to behave similarly to NSSegmentDistributionFit
Update: here is workaround, based on finding NSSegmentedControl in run-time view hierarchy.
Disclaimer: Actually it is safe, ie. no crash in run-time, but can stop working in future returning to default behaviour.
So, the idea is to inject NSView via representable into view hierarchy above (!!) Picker, as
Picker(selection: $store.utility.saliencyType, label: EmptyView()) {
ForEach(Store.Utility.SaliencyType.allCases, id: \.self) { saliencyType in
Text(saliencyType.text)
.tag(saliencyType)
}
}
.overlay(NSPickerConfigurator { // << here !!
$0.segmentDistribution = .fillEqually // change style !!
})
.pickerStyle(SegmentedPickerStyle())
and configurator itself
struct NSPickerConfigurator: NSViewRepresentable {
var configure: (NSSegmentedControl) -> Void
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
if let holder = view.superview?.superview {
let subviews = holder.subviews
if let nsSegmented = subviews.first?.subviews.first as? NSSegmentedControl {
self.configure(nsSegmented)
}
}
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
Ah the reach down to AppKit method.
Very clever indeed.
However this is not working for me, Monteray 12.3
Went to debug further using Xcode's Visual Debugger and I can see the NSPickerConfigurator class in the view hierarchy but no NSSegmetedControl.
It appears as if apple is clearing up NSViews from the hierarchy.
Time to think pure swiftui.