How to Make NSVisualEffectView That Only Appears When Content is Beneath It - swift

I am working on a macOS application that makes use of NSVisualEffectView to achieve transparency. I want to achieve an effect similar to that of iOS' Cover Sheet widget view. Notice how when you scroll up, content a blur appears around the search bar. I want do this in a macOS app so that when content in a table view is scrolled beneath a certain point an NSVisualEffectView blurs it. How would I do this? Thanks in advance.

Create a instance of Visual Effect View as following and set on window:
func setVisualEffectToWindow(window: NSWindow) {
// create the visual effect view
var blurryView = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) //Create with size you want or you can use window content bound here
// this is default value but is here for clarity
blurryView.blendingMode = NSVisualEffectBlendingMode.BehindWindow
// set the background to always be the dark blur
blurryView.material = NSVisualEffectMaterial.Dark
// set it to always be blurry regardless of window state
blurryView.state = NSVisualEffectState.Active
window.contentView.addSubview(blurryView)
}

Related

UITextView Moves to Left on Keyboard Show

I have an app with a build target of IOS 14 that is causing a problem regarding automatic positioning of the view on keyboard show.
I have a UITextView that is draggable and can be positioned partially outside of the main view that it sits within. If the field is large enough then it will extend beyond the parent view and safe area also. The parent view has clipsToBounds set as true so the overflow of the text view is not visible.
The problem is when the text field is positioned so that its right hand side is outside of the safe area and the keyboard is presented, the screen automatically scrolls left to include the far right edge of the text view, even though it is not visible due to clipsToBounds being set on its parent. I need to disable the behaviour that is causing this to happen but can't find anything that covers this for UIKit.
See below for a visual example. Can anybody please help?
Image 1
Image 2
Edit:
The structure of the screen is:
View Controller:
.....UICollectionView:
..........UICollectionViewCell:
...............UIView:
....................Elements (UITextView in this case)
func calculateCarouselOffset(formHeight: CGFloat) -> CGAffineTransform {
let carouselOffset: CGAffineTransform!
let currentElementMaxY = returnCurrentElementMaxY()
let elementMaxYTransformRemoved = currentElementMaxY + -self.scalingCarousel.transform.ty
let newFormOriginY = safeAreaFrame.height - formHeight
let topOfFormMargin: CGFloat = 20
if (newFormOriginY - topOfFormMargin) < elementMaxYTransformRemoved {
// Form will overlap element - move carousel view to compensate
let oldToNewLocDist = (newFormOriginY - topOfFormMargin) - currentElementMaxY
let moveScreenBy = self.scalingCarousel.transform.ty + oldToNewLocDist
carouselOffset = CGAffineTransform(translationX: 0, y: moveScreenBy)
} else {
// Form will not overlap element - reset carousel view
carouselOffset = self.formDeactivate
}
return carouselOffset
}
And it is called as below:
func textViewDidChage() {
let backgorundTransform = calculateCarouselOffset(formHeight: currentElementFormHeight)
let modifyBackground = UIViewPropertyAnimator(duration: 0.2, curve: .linear, animations: {
self.scalingCarousel.transform = backgorundTransform
})
modifyBackground.startAnimation()
}
It looks like this is (possibly new?) built-in behaviour for text fields. I reproduced this both with a collection view controller and a view controller holding a collection view. The text field moves itself to visible like this:
I found this by adding a symbolic breakpoint on contentOffset and then making a field editable - there are a lot of calls before you get to this point because it's also adjusting things for the keyboard coming up.
Unfortunately in your case, I think the scroll view is moving the text field's visible bounds into the visible area, which means you're scrolling horizontally since the text field is off screen.
You can't override scrollTextFieldToVisibleIfNecessary as it is private API. There are probably some hacks you can do by overriding becomeFirstResponder but they seem quite likely to either not work, or break other things.

Is there a way to move a SwiftUI view contained within a UIHostingController into the NavBar Space

My current implementation appends the UIHostingController below the NavBar. But I want the UIhostingController to begin at the end of the status bar (the gray area below the notch). Here is the code and resulting image:
self.reviewHomePage = ReviewHomePage(reviewDict: reviewDict)
let hostingController = UIHostingController(rootView: self.reviewHomePage)
self.addChild(hostingController)
hostingController.view.frame = self.view.frame
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
I have tried to hide the NavBar using the various techniques discussed here: Storyboard - Hiding top bar of navigation controller programmatically. But after hiding the NavBar, the space the NavBar occupies is still reserved for the NavBar (the background color is just changed to gray). I have tried to adjust the frame to start within the navBar space with the following code:
let navHeight = self.navigationController!.navigationBar.frame.minY
let topFullView = ((self.navigationController?.view.frame.minY)!) + (navHeight)
let reviewFrame = CGRect(x: (self.navigationController?.view.frame.minX)!,
y: (topFullView),
width: self.navigationController!.view.frame.width,
height: self.navigationController!.view.frame.height - topFullView)
But the UIHostingController is moved up behind the NavBar as shown in the image below.
Is it possible to completely remove the NavBar or append the view on top of the NavBar while maintaining the TabBar at the bottom of the screen? I appreciate the help!

UIButton action is not triggered after constraint layouts changed

I have got an UIButton on a storyboard ViewController. When I load data into the form and the layout is significantly changing the button does not recognise the touch action.
I have figured out that when button is visible on the scrollview right after it if filled with data, the touch action works.
If the data too long and the button is not visible at first, just when it is scrolled into the display, the touch action does not work.
I was checking if something is above the button, but nothing. I have tried to change the zPosition of the button, not solved the problem.
What can be the issue?
I have made custom classes from the UIScrollView and the UIButton to check how the touches event triggered. It is showing the same behaviour, which is obvious. If the button is visible right at the beginning, the UIButton's touchesBegan event is triggered. If the button moves down and not visible at the beginning, it is never triggered, but the scrollview's touchesBegan is called instead.
Depending on the size of the data I load into the page sometimes the button is visible at the beginning, but the form can be still scrolled a bit. In this case the button still work, so it seems that this behaviour is not depending on if the scrollview is scrolled before or not, just on the initial visibility of the button.
Is there any layout or display refresh function which should be called to set back the behaviour to the button?
The code portion which ensures that the contentview is resized for the scroll if the filled data requires bigger space.
func fillFormWithData() {
dispDescription.text = jSonData[0]["advdescription"]
dispLongDescription.text = jSonData[0]["advlongdesc"]
priceandcurrency.text = jSonData[0]["advprice"]! + " " + jSonData[0]["advpricecur"]!
validitydate.text = jSonData[0]["advdate"]!
contentview.layoutIfNeeded()
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
contentview.frame.size.height = contentRect.size.height
scrollview.contentSize = contentview.bounds.size
}
Ok, so another update. I have coloured the contentview background to blue and the scrollview background to white. When I load the data and resize the layout constraints, the contentview is resizing as expected, however now the scrollview is going to the bottom. After I scroll the view it is resizing to the original size which fits the screen. Now the button is only recognised when I touch the are which is blue behind. With the white background it is not recognised anymore, so it seems that the scrollview is hiding the button.
Let me get this clear the button is added in storyboard and it is a spritekit project?? If you are using zPosition?? Why don’t u connect the UIButton via the assistant editor as an IBAction then the action is always tied to the button.
You can also do it differently
Create an SKLabelNode and put it on the screen where you want to have the button and then set a name to it as myButton
override func touchesBegan(_ touches: Set<UITouch>, with event:
UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let tappedNodes = nodes(at: location)
for node in tappedNodes {
if node.name == "myButton" {
// call your action here
}
}
}
}
EDIT 1:
You could also try auto resizing your scrollView.content this works also if you are adding any views via the app or programmatically
private func resizeScrollView(){
print("RESIZING THE SCROLLVIEW from \(scrollView.contentSize)")
for view in scrollView.subviews {
contentRect = contentRect.union(view.frame)
}
scrollView.contentSize = CGSize(width: contentRect.size.width, height: contentRect.size.height + 150)
print("THE CONTENT SIZE AFTER RESIZING IS: \(scrollView.contentSize)")
}
EDIT 2: I think I found the issue with your project. You need to move the MessageButton(UzenetButton) above DispDescription label in the object inspector in that way it will always be above your message textView.
At the moment the UzeneButton is at the very far back in your view hierarchy so if your textView is resizing whilst editing it covers the button that is why you cannot click on it.
See #Endre Olah,
To make situation more clear do one more thing, set clipToBound property of contentview to true.
you will notice that after loading of data your button not fully visible, it means it is shifting out of bound of its parentView (ContentView)
And that's why button is not taking your touch. However, if you carefully touch upper part of button it still do its job. Because upper part is still in bound of ContentView
Solution :
After loading of data you have to make sure that you increase height of ContentView such that button should never go out of bound of its parentView(ContentView).
FOR EXAMPLE
#IBOutlet var heightConstraintOfContentView : NSLayoutConstraint!
After loading of data
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
heightConstraintOfContentView.constant = contentRect.size.height
contentView.layoutIfNeeded()
I use following steps when I need to use scrollview with dynamic content:
1) Firstly add a scrollView with top, bottom, trailing and leading is 0 to super view.
2) Add a view to scrollView and view's trailing, leading bottom and top space to scrollView can be set to 0 (or you can add margin optionally).
3) Now, you should add UI elements like buttons, labels with appropriate top, bottom, trailing and leading margins to each other.
4) Lastly, add equal height and equal width constraint to view with Safe Area:
and change equal height priority of view to 250:
It should solve your problem with UIScrollView.
Finally, I have found the solution in another chain, once it became clear that the scrollview's contentview is resizing on scroll event to the original size. (Not clear why this is like this, but that is the fact.)
So I had to add a height constraint to the contentview in the storyboard and create an outlet to it and adjust this constraint when the content size is changing, like this:
#IBOutlet weak var ContentViewHeight: NSLayoutConstraint!
func fillFormWithData() {
dispDescription.text = jSonData[0]["advdescription"]
dispLongDescription.text = jSonData[0]["advlongdesc"]
priceandcurrency.text = jSonData[0]["advprice"]! + " " + jSonData[0]["advpricecur"]!
validitydate.text = jSonData[0]["advdate"]!
contentview.layoutIfNeeded()
let contentRect = CGRect(x: 0, y: 0, width: scrollview.frame.width, height: uzenetbutton.frame.origin.y+uzenetbutton.frame.height+50)
contentview.bounds = contentRect
scrollview.contentSize = contentRect.size
----------- This is the key line to the success ----------
ContentViewHeight.constant = contentRect.size.height
----------------------------------------------------------
}
After this is added, it works perfectly.

Changing Label/Button Text colour dynamically based on the dynamically changing background

I know it may sound a little bit of a drag to ask this question but I am curious if anything like this is available.
I am building a app in which the background image (covers full view with a blur effect) of each view controller changes dynamically. This background image will be in all the view controllers, each one having a different set of UIControls (Labels, Buttons, Table Views, Collection Views, containers, tabs, etc).
Sometimes when the background image is very light, the foreground texts (labels, buttons) with white text colour are not visible at all. Also the vice versa is a problem too.
So I would like to know if there is any way to change the foreground text colour dynamically based on its background.
Recently I faced the same problem, And I think what you looking for is either image is bright or dark so that you can set property accordingly Hope this will help.
Create observer, everytime image change it will check if its a dark image or bright and based on that will call the function UIForDarkImage and UIForBrightImage
//ImageView observer to observe the image change and perform the UI action based on the image colour
//Your imageView you are using to set image
var imageView: UIImageView {
didSet {
if imageView.isDark(image.bounds) { //dont pass the full image bounds pass the rect where your buttons or label places it will save your hell lot of time
setUIForDarkImage() // here you set buttons and labels color to white or whatever changes you want to perform based dark image
}
else {
setUIForBrightImage() // here you set buttons and labels color to black or whatever changes you want to perform based bright image
}
}
}
UIImageViewExtension for checking if Image is a dark image or bright Image.
What happening is, It will go through image pixel-by-pixel and check if the pixel is bright or dark and if we get dark pixels more than the threshold we have set, we will assuming that its a dark image else it's a bright image.
PS: For better efficiency not checking the whole image (for a high-resolution image it will slow down if we will check all pixels) so only checking the part of the image in which we need our button or label, you can set rect based on your requirement. And also scaling by 0.45 to check few pixels in that rect(you can increase or decrease for more/less accuracy).
extension UIImageView {
func isDark(_ rect:CGRect)->Bool {
let s=image?.cgImage?.cropping(to: rect);
let data=s?.dataProvider?.data;
if data == nil {return false;}
guard let ptr = CFDataGetBytePtr(data) else {return false;}
let threshold = Int(Double(rect.width * rect.height) * 0.45);
var dark = 0,len=CFDataGetLength(data);
for i in stride(from: 0, to: len, by: 4) {
let r = ptr[i], g = ptr[i+1], b = ptr[i+2];
if (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) < 100 {dark += 1;}
if dark > threshold {return true;}
}
return false;
}
}
PS: If you also like to know what I am doing in setUIForBrightImage() and setUIForDarkImage do let me know
Here's what I would do: Create a struct that contains the name of the image view and a tint color to use for your labels, buttons, etc.
Add a property to your view controllers using this new struct. Let's call it backgroundImageSettings.
Add a didSet method to the property that calls another method useBackgroundImageSettings() that installs the image into the background of the view controller's content view, sets the tint color for the view controller's content view, and calls setNeedsDisplay() on the content view.. Also call useBackgroundImageSettings() in your viewDidLoad so that setting your backgroundImageSettings still works even if you view controller's view hasn't been loaded yet.
I like Duncan's answer! An additional step you can take is to set a slight overlay onto your background image.
For example, if you have a background image with really dark colors and you can't see your black text very well, then you can add a layer with a background color of white and an opacity less than 1. That way there's a bit of white to make your text more readable, and you can still see the background image.
let overlay = UIView(frame: yourSuperView.bounds)
overlay.backgroundColor = UIColor.white
overlay.layer.opacity = 0.5
yourSuperView.addSubview(overlay)

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.