How to fix the wrong frame after device rotation? - swift

I use custom alert view in my application. When called, its frame completely fills the screen. However, after the device rotates, it does not look right.
Then I try to change my alert frame and assign it a new screen size in viewWillTransition
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: nil) { _ in UIView.setAnimationsEnabled(true) }
UIView.setAnimationsEnabled(false)
super.viewWillTransition(to: size, with: coordinator)
if let _ = alert {
alert?.rotateAlert(to: size)
Logger.Log("after size = \(alert?.backgroundView.frame.size)")
Logger.Log("after frame = \(alert?.frame)")
Logger.Log("after bounds = \(alert?.bounds)")
}
I tried to do the same in viewDidLayoutSubviews
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let _ = alert {
Logger.Log("viewDidLayoutSubviews before rotation")
alert?.rotateAlert(to: self.view.frame.size)
Logger.Log("after size = \(alert?.backgroundView.frame.size)")
Logger.Log("after frame = \(alert?.frame)")
Logger.Log("after bounds = \(alert?.bounds)")
}
}
func rotateAlert(to size: CGSize) {
self.frame.size = size
backgroundView.frame = frame
}
Thus the frame of my alert changes. But its size is still smaller than the screen in height, thus forming an empty area.
Why is this happening? And how do I fix this? (I use Swift4)

To place your alert in the center of the screen, assuming that it is an instance of UIView, do this:
alert.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
alert.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// Or
NSLayoutConstraint.activate([
alert.centerXAnchor.constraint(equalTo: view.centerXAnchor),
alert.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
This is using auto layout to position your views, and the views will properly update when the trait collection changes.

When adding a view programmatically the view doesn't adjust according to the screen rotation because you have fixed the view height, width so after rotation you need to again give its frame.
This doesn't happen when you use constraints to add a view either programmatically or from the storyboard
For your alert to resize you have to again give its frame in viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
YourAlertView.frame = CGRect(x:0 , y: 0,width: self.view.frame.width, height: self.view.frame.height )
}
Works like a charm for me.

Related

Updating variable in view constraint (SnapKit)

View is initialized with following constraints
View.snp.makeConstraints { (para) in
View.topConstraint = para.top.equalTo(parentview.snp.top).constraint
View.LeadingConstraint = para.leading.equalTo(parentview.snp.leading).constraint
View.TrailingConstraint = para.trailing.equalTo(parentview.snp.trailing).constraint
View.BottomConstraint =para.bottom.equalTo(parentview.snp.bottom).offset(-getheight).constraint
}
where getheight = parentview.frame.size.height/2 ;
when parentview changes its dimensions.View doesnt update its height as constraints are not called again.
any way to update or recall its constraints other the remakingConstraint which is not feasible at large scale.
Have tried:
View.updateConstraints()
View.setNeedsUpdateConstraints()
View.setNeedsLayout()
I need reference to each constraints because
if View.bottomTouch {
View.bottomConstraint.update(offset: View. BottomConstraint.layoutConstraints[0].constant + CurrentPoint - PreviousPoint)
}
Is there a reason you don't want to use 50% of the parent view height?
View.snp.makeConstraints { (para) in
para.top.equalTo(parentview.snp.top)
para.leading.equalTo(parentview.snp.leading)
para.trailing.equalTo(parentview.snp.trailing)
// 50% of the parent view height
para.height.equalTo(parentview.snp.height).multipliedBy(0.5)
// instead of this
//para.bottom.equalTo(parentview.snp.bottom).offset(-getheight)
}
Edit - after comments...
Keeping a reference to a constraint for the purposes of dragging a view is a very different question from "Keep the child view at 50% of the height of the parent view."
Give this a try...
It will create a cyan "parentView" with a blue "childView" (subview). Dragging the blue view (Pan Gesture) will drag its bottom up / down. Tapping anywhere (Tap Gesture) will toggle the insets on the frame of the parentView between 20 and 60.
When the parentView frame changes - either from the tap or, for example, on device rotation - the "childView" bottom will be reset to 50% of the height of the "parentView":
class ViewController: UIViewController {
let parentView = UIView()
let childView = UIView()
// childView bottom constraint
var bc: Constraint!
override func viewDidLoad() {
super.viewDidLoad()
parentView.backgroundColor = .cyan
childView.backgroundColor = .blue
parentView.addSubview(childView)
view.addSubview(parentView)
parentView.snp.makeConstraints { para in
para.top.leading.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide).inset(20.0)
}
// childView's bottom constraint offset will be set in viewDidLayoutSubviews()
childView.snp.makeConstraints { para in
para.top.leading.trailing.equalToSuperview()
bc = para.bottom.equalToSuperview().constraint
}
let p = UIPanGestureRecognizer(target: self, action: #selector(panHandler(_:)))
childView.addGestureRecognizer(p)
let t = UITapGestureRecognizer(target: self, action: #selector(tapHandler(_:)))
view.addGestureRecognizer(t)
}
#objc func tapHandler(_ g: UITapGestureRecognizer) -> Void {
// on tap, toggle parentView inset
// between 20 and 60
// this will trigger viewDidLayoutSubviews(), where the childView bottom
// constraint will be reset to 50% of the parentView height
var i: CGFloat = 60.0
if parentView.frame.origin.x > 20 {
i = 20.0
}
parentView.snp.updateConstraints { para in
para.top.leading.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide).inset(i)
}
}
#objc func panHandler(_ g: UIPanGestureRecognizer) -> Void {
let translation = g.translation(in: g.view)
// update bottom constraint constant
bc.layoutConstraints[0].constant += translation.y
// reset gesture translation
g.setTranslation(CGPoint.zero, in: self.view)
}
var parentViewHeight: CGFloat = 0.0
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// reset childView's bottom constraint
// to 50% of its superView's height
// ONLY if parentView frame height has changed
if parentView.frame.height != parentViewHeight {
parentViewHeight = parentView.frame.height
bc.layoutConstraints[0].constant = -parentViewHeight * 0.5
}
}
}
Firstly, check whether the getheight value did update when the parent view layout change. In order to reload the existing constraints, you may need to call layoutIfNeeded() of your parent view.

How to resize a NSTextView automatically as per its content?

I am making an app where a user can click anywhere on the window and a NSTextView is added programmatically at the mouse location. I have got it working with the below code but I want this NSTextView to horizontally expand until it reaches the edge of the screen and then grow vertically. It currently has a fixed width and when I add more characters, the text view grows vertically (as expected) but I also want it to grow horizontally. How can I achieve this?
I have tried setting isHorizontallyResizable and isVerticallyResizable to true but this doesn't work. After researching for a while, I came across this https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextStorageLayer/Tasks/TrackingSize.html but this didn't work for me either.
Code in my ViewController to add the NSTextView to its view:
private func addText(at point: NSPoint) {
let textView = MyTextView(frame: NSRect(origin: point, size: CGSize(width: 150.0, height: 40.0)))
view.addSubview(textView)
}
And, MyTextView class looks like below:
class MyTextView: NSTextView {
override func viewWillDraw() {
isHorizontallyResizable = true
isVerticallyResizable = true
isRichText = false
}
}
I have also seen this answer https://stackoverflow.com/a/54228147/1385441 but I am not fully sure how to implement it. I have added this code snippet in MyTextView and used it like:
override func didChangeText() {
frame.size = contentSize
}
However, I think I am using it incorrectly. Ergo, any help would be much appreciated.
I'm a bit puzzled, because you're adding NSTextView to a NSView which is part of the NSViewController and then you're talking about the screen width. Is this part of your Presentify - Screen Annotation application? If yes, you have a full screen overlay window and you can get the size from it (or from the view controller's view).
view.bounds.size // view controller's view size
view.window?.frame.size // window size
If not and you really need to know the screen size, check the NSWindow & NSScreen.
view.window?.screen?.frame.size // screen size
Growing NSTextView
There's no any window/view controller's view resizing behavior specified.
import Cocoa
class BorderedTextView: NSTextView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path = NSBezierPath(rect: bounds)
NSColor.red.setStroke()
path.stroke()
}
}
class ViewController: NSViewController {
override func mouseUp(with event: NSEvent) {
// Convert point to the view coordinates
let point = view.convert(event.locationInWindow, from: nil)
// Initial size
let size = CGSize(width: 100, height: 25)
// Maximum text view width
let maxWidth = view.bounds.size.width - point.x // <----
let textView = BorderedTextView(frame: NSRect(origin: point, size: size))
textView.insertionPointColor = .orange
textView.drawsBackground = false
textView.textColor = .white
textView.isRichText = false
textView.allowsUndo = false
textView.font = NSFont.systemFont(ofSize: 20.0)
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = true
textView.textContainer?.widthTracksTextView = false
textView.textContainer?.heightTracksTextView = false
textView.textContainer?.size.width = maxWidth // <----
textView.maxSize = NSSize(width: maxWidth, height: 10000) // <----
view.addSubview(textView)
view.window?.makeFirstResponder(textView)
}
}
I finally got it to work (except for one minor thing). The link from Apple was the key here but they haven't described the code completely, unfortunately.
The below code work for me:
class MyTextView: NSTextView {
override func viewWillDraw() {
// for making the text view expand horizontally
textContainer?.heightTracksTextView = false
textContainer?.widthTracksTextView = false
textContainer?.size.width = 10000.0
maxSize = NSSize(width: 10000.0, height: 10000.0)
isHorizontallyResizable = true
isVerticallyResizable = true
isRichText = false
}
}
That one minor thing which I haven't been able to figure out yet is to limit expanding horizontally until the edge of the screen is reached. Right now it keeps on expanding even beyond the screen width and, in turn, the text is hidden after the screen width.
I think if I can somehow get the screen window width then I can replace 10000.0 with the screen width (minus the distance of text view from left edge) and I can limit the horizontal expansion until the edge of the screen. Having said that, keeping it 10000.0 won't impact performance as described in the Apple docs.

Identifying Objects in Firebase PreBuilt UI in Swift

FirebaseUI has a nice pre-buit UI for Swift. I'm trying to position an image view above the login buttons on the bottom. In the example below, the imageView is the "Hackathon" logo. Any logo should be able to show in this, if it's called "logo", since this shows the image as aspectFit.
According to the Firebase docs page:
https://firebase.google.com/docs/auth/ios/firebaseui
You can customize the signin screen with this function:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return FUICustomAuthPickerViewController(nibName: "FUICustomAuthPickerViewController",
bundle: Bundle.main,
authUI: authUI)
}
Using this code & poking around with subviews in the debuggers, I've been able to identify and color code views in the image below. Unfortunately, I don't think that the "true" size of these subview frames is set until the view controller presents, so trying to access the frame size inside these functions won't give me dimensions that I can use for creating a new imageView to hold a log. Plus accessing the views with hard-coded index values like I've done below, seems like a pretty bad idea, esp. given that Google has already changed the Pre-Built UI once, adding a scroll view & breaking the code of anyone who set the pre-built UI's background color.
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
// Create an instance of the FirebaseAuth login view controller
let loginViewController = FUIAuthPickerViewController(authUI: authUI)
// Set background color to white
loginViewController.view.backgroundColor = UIColor.white
loginViewController.view.subviews[0].backgroundColor = UIColor.blue
loginViewController.view.subviews[0].subviews[0].backgroundColor = UIColor.red
loginViewController.view.subviews[0].subviews[0].tag = 999
return loginViewController
}
I did get this to work by adding a tag (999), then in the completion handler when presenting the loginViewController I hunt down tag 999 and call a function to add an imageView with a logo:
present(loginViewController, animated: true) {
if let foundView = loginViewController.view.viewWithTag(999) {
let height = foundView.frame.height
print("FOUND HEIGHT: \(height)")
self.addLogo(loginViewController: loginViewController, height: height)
}
}
func addLogo(loginViewController: UINavigationController, height: CGFloat) {
let logoFrame = CGRect(x: 0 + logoInsets, y: self.view.safeAreaInsets.top + logoInsets, width: loginViewController.view.frame.width - (logoInsets * 2), height: self.view.frame.height - height - (logoInsets * 2))
// Create the UIImageView using the frame created above & add the "logo" image
let logoImageView = UIImageView(frame: logoFrame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
// loginViewController.view.addSubview(logoImageView) // Add ImageView to the login controller's main view
loginViewController.view.addSubview(logoImageView)
}
But again, this doesn't seem safe. Is there a "safe" way to deconstruct this UI to identify the size of this button box at the bottom of the view controller (this size will vary if there are multiple login methods supported, such as Facebook, Apple, E-mail)? If I can do that in a way that avoids the hard-coding approach, above, then I think I can reliably use the dimensions of this button box to determine how much space is left in the rest of the view controller when adding an appropriately sized ImageView. Thanks!
John
This should address the issue - allowing a logo to be reliably placed above the prebuilt UI login buttons buttons + avoiding hard-coding the index values or subview locations. It should also allow for properly setting background color (also complicated when Firebase added the scroll view + login button subview).
To use: Create a subclass of FUIAuthDelegate to hold a custom view controller for the prebuilt Firebase UI.
The code will show the logo at full screen behind the buttons if there isn't a scroll view or if the class's private constant fullScreenLogo is set to false.
If both of these conditions aren't meant, the logo will show inset taking into account the class's private logoInsets constant and the safeAreaInsets. The scrollView views are set to clear so that a background image can be set, as well via the private let backgroundColor.
Call it in any signIn function you might have, after setting authUI.providers. Call would be something like this:
let loginViewController = CustomLoginScreen(authUI: authUI!)
let loginNavigationController = UINavigationController(rootViewController: loginViewController)
loginNavigationController.modalPresentationStyle = .fullScreen
present(loginNavigationController, animated: true, completion: nil)
And here's one version of the subclass:
class CustomLoginScreen: FUIAuthPickerViewController {
private var fullScreenLogo = false // false if you want logo just above login buttons
private var viewContainsButton = false
private var buttonViewHeight: CGFloat = 0.0
private let logoInsets: CGFloat = 16
private let backgroundColor = UIColor.white
private var scrollView: UIScrollView?
private var viewContainingButton: UIView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// set color of scrollView and Button view inside scrollView to clear in viewWillAppear to avoid a "color flash" when the pre-built login UI first appears
self.view.backgroundColor = UIColor.white
guard let foundScrollView = returnScrollView() else {
print("😡 Couldn't get a scrollView.")
return
}
scrollView = foundScrollView
scrollView!.backgroundColor = UIColor.clear
guard let foundViewContainingButton = returnButtonView() else {
print("😡 No views in the scrollView contain buttons.")
return
}
viewContainingButton = foundViewContainingButton
viewContainingButton!.backgroundColor = UIColor.clear
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Create the UIImageView at full screen, considering logoInsets + safeAreaInsets
let x = logoInsets
let y = view.safeAreaInsets.top + logoInsets
let width = view.frame.width - (logoInsets * 2)
let height = view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom + (logoInsets * 2))
var frame = CGRect(x: x, y: y, width: width, height: height)
let logoImageView = UIImageView(frame: frame)
logoImageView.image = UIImage(named: "logo")
logoImageView.contentMode = .scaleAspectFit // Set imageView to Aspect Fit
logoImageView.alpha = 0.0
// Only proceed with customizing the pre-built UI if you found a scrollView or you don't want a full-screen logo.
guard scrollView != nil && !fullScreenLogo else {
print("No scrollView found.")
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
self.view.addSubview(logoImageView)
self.view.sendSubviewToBack(logoImageView) // otherwise logo is on top of buttons
return
}
// update the logoImageView's frame height to subtract the height of the subview containing buttons. This way the buttons won't be on top of the logoImageView
frame = CGRect(x: x, y: y, width: width, height: height - (viewContainingButton?.frame.height ?? 0.0))
logoImageView.frame = frame
self.view.addSubview(logoImageView)
UIView.animate(withDuration: 0.25, animations: {logoImageView.alpha = 1.0})
}
private func returnScrollView() -> UIScrollView? {
var scrollViewToReturn: UIScrollView?
if self.view.subviews.count > 0 {
for subview in self.view.subviews {
if subview is UIScrollView {
scrollViewToReturn = subview as? UIScrollView
}
}
}
return scrollViewToReturn
}
private func returnButtonView() -> UIView? {
var viewContainingButton: UIView?
for view in scrollView!.subviews {
viewHasButton(view)
if viewContainsButton {
viewContainingButton = view
break
}
}
return viewContainingButton
}
private func viewHasButton(_ view: UIView) {
if view is UIButton {
viewContainsButton = true
} else if view.subviews.count > 0 {
view.subviews.forEach({viewHasButton($0)})
}
}
}
Hope this helps any who have been frustrated trying to configure the Firebase pre-built UI in Swift.

Swift - Visual constraints not working

I have a scrollView which I instantiate programaticaly. What I want to do is to add constraints so it will look good in both horizontal and vertical orientation. The problem that constraints not working
How it looks vertically - good
How it looks horizontally - not good
Code is following
class FAPhoto: UIViewController, UIScrollViewDelegate {
var imageURLsArray = [String]()
var imageViews:[UIImageView] = []
var arrayOfPhotos = [Photo]()
var scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView(frame: self.view.frame)
view.addSubview(scrollView)
scrollView.contentSize = CGSizeMake(view.frame.size.width * CGFloat(arrayOfPhotos.count), scrollView.frame.size.height)
scrollView.pagingEnabled = true
for (var i = 0; i < arrayOfPhotos.count; i++) {
var imageView = UIImageView()
imageView.frame = CGRectMake(CGFloat(i) * view.frame.size.width, scrollView.frame.origin.y, scrollView.frame.size.width, scrollView.frame.size.height)
imageView.contentMode = .ScaleAspectFit
let image = imageView.hnk_setImageFromURL(NSURL(string: arrayOfPhotos[i].url!)!)
scrollView.addSubview(imageView)
}
}
override func viewWillAppear(animated: Bool) {
}
override func viewWillLayoutSubviews() {
let bindings = Dictionary(dictionaryLiteral: ("scrollView", self.scrollView))
let horizontalConstraints =
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-0-[scrollView]-0-|",
options: [],
metrics: nil,
views: bindings)
self.view.addConstraints(horizontalConstraints)
let verticalConstraints =
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-0-[scrollView]-0-|",
options: [],
metrics: nil,
views: bindings)
self.view.addConstraints(verticalConstraints)
}
}
The problem is that you set contentSize of scrollView and imageView's frames in viewDidLoad. This is OK for portrait mode, but when view rotates to landscape, it's frame also changes, so you have to update these values accordingly. I think you have 3 opportunities here.
Track rotation events and update contentSize of scrollView and imageView's frames, when view rotates.
Use Autolayout for scrollView. You will not have to calculate contentSize or set imageView's frames, just set constraints once in viewDidLoad. Here you can find some examples https://stackoverflow.com/a/20232911/4757335.
Use UICollectionView instead of UIScrollView. It handles rotation much easier.

adding NSImageView to NSScrollView

I have an image with a dimension of about 200x2000 pixels. I want to display the image centered in a 200x200 rectangle but I want to be able to move it up and down. Best I can figure I need to add an NSImageView to an NSScrollView but I can't figure out how or even if this is the best way. This is my first day of OS X development...
After some googling I found this from which I was able to come up with this
class MasterViewController: NSViewController {
var Photo: NSImageView!
#IBOutlet var scroll: NSScrollView!
override func viewDidLoad() {
super.viewDidLoad()
var imageRect: NSRect
self.Photo = NSImageView.init()
self.Photo.image = NSImage.init(named:"horizon")
imageRect = NSMakeRect(0.0, 0.0, self.Photo.image!.size.width, self.Photo.image!.size.height)
print("image size", imageRect)
self.Photo = NSImageView(frame: imageRect)
self.Photo.setBoundsSize(NSSize(width: imageRect.width, height: imageRect.height))
self.Photo.imageScaling = NSImageScaling.ScaleNone
self.scroll.setFrameSize(NSSize(width: imageRect.width,height: imageRect.width))
self.scroll.hasVerticalScroller = true
self.scroll.hasHorizontalScroller = true
self.Photo.setFrameSize(CGSize(width: imageRect.width,height: imageRect.width))
self.scroll.documentView = self.Photo
//print(self.scroll.documentView?.frame)
//self.scroll.setC contentSize = NSSize(width: 200, height: 2000)
//self.Photo.image = NSImage.init(named:"bezel")
//self.scroll.addSubview(self.Photo)
}
but I can't get the image to show up inside the scrollview
#lusher00: in your example, you initialize twice self.Photo, first with an init() and then with (frame: imageRect), this probably explains why image don't show up.
You could set the imageView as the documentView of the NSScrollView, as below:
#IBOutlet var scrollView: NSScrollView!
var imageRect: NSRect
// Initialize below the imageView image with appropriate content (to adapt)
imageView.image = NSImage.init(named:"horizon")
imageRect = NSMakeRect(0.0, 0.0, imageView.image!.size.width, imageView.image!.size.height)
imageView.setFrameSize(CGSize(width: imageRect.width, height: imageRect.height))
imageView.imageScaling = NSImageScaling.ScaleNone
scrollView.documentView = imageView
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = true
Just add the image view as a subview of the scrollview.
scrollView.addSubview(imageView)
You can set the position of the imageview, which will position it inside the scrollview.
The scrollview needs to know how large its content size is to enable scrolling of that area. So dont forget to update the contentSize property of the scrollview.
E.g. adding the imageView of 200x200 to the scrollView with a frame of 200x200. Setting the contentSize to 400x400 and calling imageView.center = scrollView.center, will center the image and allow some scrolling around the image, within the 200x200 visible frame of the scrollview.
You can also get the current offset of the scrollView by checking contentOffset.
If you need to track as the user scrolls, you can use scrollViewDidScroll. Check the docs for the scrollview for some other options.