UIPopoverPresentationController without overlay - swift

I am implementing a popover view using UIPopoverPresentationController.
The trouble with this, is that by default, I have a shadow with a large radius for the controller.
I want to disable this - the overlay.
I have tried:
to customise the layout shadow (using a UIPopoverBackgroundView):
layer.shadowColor = UIColor.white.withAlphaComponent(0.01).cgColor
layer.shadowOffset = .zero
layer.shadowRadius = 0
In view debugging - I can see behind the popup 4 image views with gray gradient background:
I am sure this is a default behaviour, of showing an overlay behind a popover.
How do disable this?
I found this and this. But those didn't helped.

If you take a closer look at the views hierarchy you will notice that the shadow layer _UIMirrorNinePatchView is a sublayer of UITransitionView same as UIPopoverView - both are on the same level.
views hierarchy picture
In this case you can try to hide this sublayer like so:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let shadowLayer = UIApplication.shared.windows.first?.layer.sublayers?[1].sublayers?[1] {
shadowLayer.isHidden = true
}
}
Make sure to hide it in viewDidLayoutSubviews to avoid exceptions related to missing sublayers or sublayer flickering.

Related

Set UILabel opaque on transparent UIView

I have view with layer.opacity = 0.84
This view contains a few UILabel. And these labels a bit transparent. My question is: how i can make opaque UILabel's and half-transparent UIView?
What i try to do:
lbl.layer.opacity = 1
add to view with layer.opacity = 0.84 a new one with backgroundColor = .clear and attach my labels to the second view (with clear background)
Thank you
ok guys, i found solution
the logic
make main view backgroundColor = .clear
add a new view to main. But do not use constraints, use frame
in a new view set .alpha = 0.84
attach labels to main view, not new view
example
// self its main view
self.backgroundColor = .clear
let blurView = BlurViewMaker.createBlurView(with: self.bounds)
blurView.alpha = 0.84
self.addSubview(blurView)
self.addSubview(lblEventName)
self.addSubview(lblEventDate)
self.addSubview(lblEventTag)
// do constraints from labels to self
If you apply an opacity to a layer (or a view) all sublayers are affected, and will be at least that transparent. If sublayers are also < 100% opaque, the transparency will be cumulative.
Instead, I would set your base layer's isOpaque property to false and fill it with partly transparent gray, leave the layer's opacity at 1.0, and then add one or more fully opaque sublayers containing your text content.

How to make a nav bar transparent before scrolling (iOS 14)

I'd like to implement a nav style like what is found in the "Add Reminder" view controller from Apple's Reminders (iOS 14). I've tried hooking into the scrollview delegate methods but I'm not sure how to change the alpha of the default nav bar background/shadow image.
I've tried changing the nav bar style on scroll and, while that works, it doesn't fade in/out like in the example. That makes me think the answer lies manipulating the alpha value. Thanks in advance!
I've found a (hacky) solution that works in iOS 14 (untested in other versions). It makes an assumption about the private view structure of UINavigationBar, so there's no guarantee that it will work in future iOS versions, but it's unlikely to crash - the worst that should happen is that the bar will fail to hide, or only partially hide.
Assuming that you are placing the code inside a UIViewController subclass that it acting as the delegate for a UITableView, UICollectionView or UIScrollView, the following should work:
override func viewDidLoad() {
super.viewDidLoad()
// this hides the bar initially
self.navigationController?.navigationBar.subviews.first?.alpha = 0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let navigationController = self.navigationController else { return }
let navBarHeight = navigationController.navigationBar.frame.height
let threshold: CGFloat = 20 // distance from bar where fade-in begins
let alpha = (scrollView.contentOffset.y + navBarHeight + threshold) / threshold
navigationController.navigationBar.subviews.first?.alpha = alpha
}
The magic threshold value is a little hard to explain, but it's basically the distance from the bar at which the fade in will start. A value of 20 means the bar starts to fade in when the scrollView content is 20 points away. A value of 0 would mean the bar snaps straight from fully transparent to fully opaque the moment the scrollView content touches it.

How to obtain a screen flash effect like screen capture effect?

Is there any way to obtain a flash screen effect like the one produced when a screenshot is taken, on demand, for a certain NSView? My question is not a duplicate of flashing screen programatically with Swift (on 'screenshot taken') , since I need a solution for osx, not ios and the methods are different.
Something like this might work
func showScreenshotEffect() {
let snapshotView = UIView()
snapshotView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(snapshotView)
// Activate full screen constraints
let constraints:[NSLayoutConstraint] = [
snapshotView.topAnchor.constraint(equalTo: view.topAnchor),
snapshotView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
snapshotView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
snapshotView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
// White because it's the brightest color
snapshotView.backgroundColor = UIColor.white
// Animate the alpha to 0 to simulate flash
UIView.animate(withDuration: 0.2, animations: {
snapshotView.alpha = 0
}) { _ in
// Once animation completed, remove it from view.
snapshotView.removeFromSuperview()
}
}
The way to achieve this is to create a new UIView that is the same size of the screen, and black in color, add it a subview of your view then animate the alpha to zero( play around with the duration to achieve the desired effect) after completion remove the view from superview.
I have used this technique in many of my projects and it works like a charm. You can play around with the background color of the view to customize the look of the flash.

swift - frame issue in UINavigationController class when rotating

I've used a custom UINavigationController class to put a colour gradient across the navbar. The custom navclass determines the frame size of the navbar, then puts the gradient inside. Problem is, when I rotate from portrait to landscape, the gradient only fills the portrait portion of the landscape bar. I've only assigned the custom UINavigationController class to the navigation view in the storyboard.
So I'm guessing I somehow need to call a refresh for the frame size in the custom navclass when a rotation is done, but I'm not sure how or where?
Here's the relevant code snippet, from the override func viewDidLoad() of the custom navclass.
let gradientlayer = CAGradientLayer()
gradientLayer.frame = self.navigationBar.bounds
self.navigationBar.layer.inserSublayer(gradientLayer, atIndex: 1)
I tried putting it inside viewWillAppear(), but that didn't work. Any help?
viewDidLoad() and viewWillAppear() don't get called on rotation, so your code won't update the frame there.
You should add gradientLayer.frame = self.navigationBar.bounds to viewWillLayoutSubviews() but leave the other code in viewDidLoad() so you don't wind up with multiple gradient layers.

NSScrollView with unclipped content view?

Is there a way I can set my scrollview not to clip its contents? (Which is a NSTextView)
I have a subclass of NSScrollView and want its content not to be clipped to its bounds.
I have tried overriding:
- (BOOL) wantsDefaultClipping{
return NO;
}
in MyScrollView and in MytextView without any effect.
In the iOS I would simply would do: myuitextView.clipsToBounds=NO; how can I do this in Cocoa?
EDIT
This is an example of what I want to achieve but in the mac
The scrollview is white, the scroller will never go outside its bounds but the text does since I did myuitextView.clipsToBounds=NO
See picture here
EDIT2
I wouldn't mind clip my view like #Josh suggested. But the real behaviour I would like to have can be explained with this picture:
Do you see the word *****EDIT***** that has being cut in the very first line?
I want the text not to be cut this way, rather I want it to completely appear and I will put a semitransparent image so it looks like it fades off when it's outside the frame.
Q: Why don't I simply put a semitransparent NSImageView on it so it looks like what I want?
A: Because 1.Scroller will be faded as well. Even if I correctly place the semitransparent NSImageView so the scroller looks fine, the cursor/caret will be able to go underneath the semitransparent NSImageView again it does not look good.
I would like to be able to control the area is clipped by NSClipView. I think that would solve my problem. Is there any alternative I have? maybe I can control the caret position or scrolling position through NSTextView so caret will never go near the top/bottom frame limits? or any work-around?
Any advice is appreciated.
Now that it's 2016 and we're using vibrant titlebars with full size content views, I'll add my thoughts to how someone might accomplish this. Hopefully, this will help anyone who came here looking for help on this, as it helped me.
This answers the question in regards to scrolling under the titlebar, but you could easily modify this technique to scroll under other things using the insets and caret position.
To get a scroll view (with or without an NSTextView inside of it) to scroll behind a titlebar, you can use:
// For transparent title.
window.titlebarAppearsTransparent = true
window.styleMask = window.styleMask | NSFullSizeContentViewWindowMask
window.appearance = NSAppearance(named: NSAppearanceNameVibrantLight)
This effectively overlays the titlebar of the NSWindow onto the window's contentView.
To constrain something to the top of the window without knowing the height of the titlebar:
// Make a constraint for SOMEVIEW to the top layout guide of the window:
let topEdgeConstraint = NSLayoutConstraint(
item: SOMEVIEW, attribute: NSLayoutAttribute.Top,
relatedBy: NSLayoutRelation.Equal,
toItem: window.contentLayoutGuide,
attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0.0)
// Turn the constraint on automatically:
topEdgeConstraint.active = true
This allows you to constrain the top of an element to the bottom of the titlebar (and or toolbar + any accessory views it may have). This was shown at WWDC in 2015: https://developer.apple.com/videos/play/wwdc2014/220/
To get the scrollview to scroll under the titlebar but show its scrollbars inside the unobscured part of the window, pin it to the top of the content view in IB or via code, which will cause it to be under the titlebar. Then, tell it to automatically update it's insets:
scrollView.automaticallyAdjustsContentInsets = true
Finally, you can subclass your window and handle the cursor/caret position. There is a presumed bug (or developer error on my part) that doesn't make the scrollview always scroll to the cursor/caret when it goes above or below the content insets of the scrollview.
To fix this, you must manually find the caret position and scroll to see it when the selection changes. Forgive my awful code, but it seems to get the job done. This code belongs in an NSWindow subclass, so self is referring to the window.
// MARK: NSTextViewDelegate
func textViewDidChangeSelection(notification: NSNotification) {
scrollIfCaretIsObscured()
textView.needsDisplay = true // Prevents a selection rendering glitch from sticking around
}
// MARK: My Scrolling Functions
func scrollIfCaretIsObscured() {
let rect = caretRectInWindow()
let y: CGFloat = caretYPositionInWindow() - rect.height
// Todo: Make this consider the text view's ruler height, if present:
let tbHeight: CGFloat
if textView.rulerVisible {
// Ruler is shown:
tbHeight = (try! titlebarHeight()) + textViewRulerHeight
} else {
// Ruler is hidden
tbHeight = try! titlebarHeight()
}
if y <= tbHeight {
scrollToCursor()
}
}
func caretYPositionInWindow() -> CGFloat {
let caretRectInWin: NSRect = caretRectInWindow()
let caretYPosInWin: CGFloat = self.contentView!.frame.height - caretRectInWin.origin.y
return caretYPosInWin
}
func caretRectInWindow() -> CGRect {
// My own version of something based off of an old, outdated
// answer on stack overflow.
// Credit: http://stackoverflow.com/questions/6948914/nspopover-below-caret-in-nstextview
let caretRect: NSRect = textView.firstRectForCharacterRange(textView.selectedRange(), actualRange: nil)
let caretRectInWin: NSRect = self.convertRectFromScreen(caretRect)
return caretRectInWin
}
/// Scrolls to the current caret position inside the text view.
/// - Parameter textView: The specified text view to work with.
func scrollToCursor() {
let caretRectInScreenCoords = textView.firstRectForCharacterRange(textView.selectedRange(), actualRange: nil)
let caretRectInWindowCoords = self.convertRectFromScreen(caretRectInScreenCoords)
let caretRectInTextView = textView.convertRect(caretRectInWindowCoords, fromView: nil)
textView.scrollRectToVisible(caretRectInTextView)
}
enum WindowErrors: ErrorType {
case CannotFindTitlebarHeight
}
/// Calculates the combined height of the titlebar and toolbar.
/// Don't try this at home.
func titlebarHeight() throws -> CGFloat {
// Try the official way first:
if self.titlebarAccessoryViewControllers.count > 0 {
let textViewInspectorBar = self.titlebarAccessoryViewControllers[0].view
if let titlebarAccessoryClipView = textViewInspectorBar.superview {
if let view = titlebarAccessoryClipView.superview {
if let titleBarView = view.superview {
let titleBarHeight: CGFloat = titleBarView.frame.height
return titleBarHeight
}
}
}
}
throw WindowErrors.CannotFindTitlebarHeight
}
Hope this helps!
I would simply try to observe the document view's frame and match the scroll view's frame when the document resizes.
This is a little hairy. AFAIK, NSViews can't draw outside their own frame. At any rate I've never seen it done, and I was somewhat surprised when I realized that UIView allows it by default. But what you probably want to do here is not manipulate clipping rectangles (doing any such thing inside NSScrollView will probably not do what you want or expect), but instead try to cover up the vertically-truncated text lines with either layers or views that are the same color as the background. Perhaps you could subclass NSClipView and override viewBoundsChanged: and/or viewFrameChanged: in order to notice when the text view is being shifted, and adjust your "shades" accordingly.
You might consider using a translucent layer to achieve this appearance, without actually drawing outside your view. I'm not certain of the rules on iOS, but on the Mac, a view drawing outside its bounds can cause interference with surrounding drawing.
However, you can set the clipping region to be whatever you like inside your scroll view subclass's drawRect: using -[NSBezierPath setClip:]:
- (void)drawRect:(NSRect)dirtyRect {
[NSGraphicsContext saveGraphicsState];
[[NSBezierPath bezierPathWithRect:[[self documentView] frame]] setClip];
//...
[NSGraphicsContext restoreGraphicsState];
}
It might be possible (since you asked) to use this code in an NSClipView subclass, but there's not much info about that, and I think you may have a hard time making it interact properly with its scroll view. If it were me, I'd try subclassing NSScrollView first.