SwiftUI - Position an overlay relative to its anchor - swift

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)

Related

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.

How to implement PencilKit in SwiftUI with background and zoom?

I am trying to build a view in SwiftUI that allows me to draw on top of another view, for example a table, using PencilKit. Everything must be zoomable and the elements of the background view must be clickable.
I thought about using a ZStack:
ZStack {
CanvasView(canvasView: $canvasView)
BackgroundView()
}
In this way I can draw on top of the BackgroundView and interact with its elements, but how can I manage the zoom? When I zoom only the canvas scales itself.
I thought about using scrollViewDidZoom:
extension Coordinator: PKCanvasViewDelegate {
func scrollViewDidZoom(_ scrollView: UIScrollView) {
onZoom()
}
}
To know when the Canvas is zooming. Then the SwiftUI View change as below:
ZStack {
CanvasView(canvasView: $canvasView, onZoom: onZoom)
BackgroundView()
.scaleEffect(scale)
}
func onZoom() {
scale = canvasView.zoomScale
}
This way the BackgroundView scales itself but only focusing on the center.
Is there a way to follow not only the canvas zoom scale but also the anchor of the zoom?
Or more generally, is there a way to get what I want?
I see many apps that permits to draw on top of some view and interact with buttons inside that view, but I cannot find any useful documentation about that.
Thank you

How to disable opacity with transitions in SwiftUI?

I'm using SwiftUI.
I have a transition that I'm using to bring up my SignInView. However, this transition seems to be automatically applying an opacity effect on the view it's replacing. This wouldn't be a problem, however, it seems like the Safe Area on both the top and the bottom have different rates of receiving the opacity than the rest of the view.
I'm trying to find 1 of 2 solutions:
How can I get rid of the opacity effect altogether, or
How can I get the opacity effect to be applied evenly everywhere.
Here is the code for my transition:
struct AuthView: View {
#State var showSignIn: Bool = false
var body: some View {
ZStack {
if !showSignIn {
WelcomeView(showSignIn: $showSignIn)
} else {
SignInView(showSignIn: $showSignIn)
.transition(AnyTransition.move(edge: .trailing))
.zIndex(1)
}
}
}
}
Here is the button controlling the state variable:
Button(action: { withAnimation(.easeInOut) { showSignIn.toggle() } }) {
//Button text
}
I also have a video (GIF) that better shows what I'm talking about, when I mention the uneven distribution of opacity.
https://i.stack.imgur.com/KXEcF.gif
If you look closely at the top and bottom safe area it fades faster than the rest of the view, which is undesired.
Note: When I changed the appearance to Dark Mode, it turned black instead of white on the top and bottom.
I assume you want identity transition for original view (opacity transition is applied by default if none other specified)
if !showSignIn {
WelcomeView(showSignIn: $showSignIn)
.transition(.identity) // << here !!

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.

Dragging NSSplitView divider does not resize views

I'm working with Cocoa and I create my views in code (no IB) and I'm hitting an issue with NSSplitView.
I have a NSSplitView that I configure in the following way in my view controller, in Swift:
override func viewDidLoad() {
super.viewDidLoad()
let splitView = NSSplitView()
splitView.isVertical = true
splitView.addArrangedSubview(self.createLeftPanel())
splitView.addArrangedSubview(self.createRightPanel())
splitView.adjustSubviews()
self.view.addSubview(splitView)
...
}
The resulting view shows the two subviews and the divider for the NSSplitView, and one view is wider than the other. When I drag the diver to change the width, as soon as I release the mouse, the divider goes back to its original position, as if pulled back by a "spring".
I can't resize the two subviews; the right one always keeps a fixed size. However, nowhere in the code I fix the width of that subview, or any of its content, to a constant.
What I would like to achieve instead is that the right view size is not fixed, and that if I drag the divider at halfway through, the subviews will resize accordingly and end up with the same width.
This is a screen recording of the problem:
Edit: here is how I set the constraints. I'm using Carthography, because otherwise setting constraints in code is extremely verbose beyond the most simple cases.
private func createLeftPanel() -> NSView {
let view = NSView()
let table = self.createTable()
view.addSubview(table)
constrain(view, table) { view, table in // Cartography magic.
table.edges == view.edges // this just constraints table.trailing to
// view.trailing, table.top to view.top, etc.
}
return view
}
private func createRightPanel() -> NSView {
let view = NSView()
let label = NSTextField(labelWithString: "Name of item")
view.addSubview(label)
constrain(view, label) { view, label in
label.edges == view.edges
}
return view
}