UIView height constraint animation, which has anchor connection with other views - xcode

Xcode swift 4
I have some views that are added dynamically in code one below another.
Each new view top anchor is connected to previous view bottom anchor.
And each view have a button that make view to expand/collapse with animation. Here is button code :
let fullHeight : CGFloat = 240
let smallHeight : CGFloat = 44
let currentHeigth = rootView.frame.size.height //I use this to get current height and understand expanded view or not
let heighCons = rootView.constraints.filter //I use this to deactivate current height Anchor constraint
{
$0.firstAttribute == NSLayoutAttribute.height
}
NSLayoutConstraint.deactivate(heighCons)
rootView.layoutIfNeeded()
if currentHeigth == smallHeight
{
rootView.heightAnchor.constraint(equalToConstant: fullHeight).isActive = true
rootView.setNeedsLayout()
UIView.animate(withDuration: 0.5)
{
rootView.layoutIfNeeded() //animation itself
}
}
else
{
rootView.heightAnchor.constraint(equalToConstant: smallHeight).isActive = true
rootView.setNeedsLayout()
UIView.animate(withDuration: 0.5)
{
rootView.layoutIfNeeded() //animation itself
}
}
This all works perfectly but i have a problem : view that below current expanding view changes it y position immediately with no animation. Its just jumping to previous view bottom anchor, that would be active after animation finish.
So my question is :
1) what is the right way to make height constraint animation, when views are connected to each other by bottom/top animation?
2) my goal is just to make a view that would expand/collapse on button click, maybe i should do it another way?

Here's an approach using Visiblity Gone Extension
extension UIView {
func visiblity(gone: Bool, dimension: CGFloat = 0.0, attribute: NSLayoutAttribute = .height) -> Void {
if let constraint = (self.constraints.filter{$0.firstAttribute == attribute}.first) {
constraint.constant = gone ? 0.0 : dimension
self.layoutIfNeeded()
self.isHidden = gone
}
}
}
Usage
expanview.visiblity(gone: true,dimension: 0)
Example
#IBOutlet weak var msgLabel: UILabel!
#IBOutlet weak var expanview: UIView!
#IBAction func toggleCollapisbleView(_ sender: UIButton) {
if sender.isSelected{
sender.isSelected = false
expanview.visiblity(gone: false,dimension: 128)
sender.setTitle("Collapse",for: .normal)
}
else{
sender.isSelected = true
expanview.visiblity(gone: true,dimension: 0)
sender.setTitle("Expand",for: .normal)
msgLabel.text = "Visiblity gone"
}
}

Related

iOS Autolayout: Oddly expand-animation of UITextView inside an UIScrollView

I'm trying to animate the height constraint of a UITextView inside a UIScrollView. When the user taps the "toggle" button, the text should appear in animation from top to bottom. But somehow UIKit fades in the complete view.
To ensure the "dynamic" height depending on the intrinsic content-size, I deactivate the height constraint set to zero.
#IBAction func toggle() {
layoutIfNeeded()
UIView.animate(withDuration: 0.6, animations: { [weak self] in
guard let self = self else {
return
}
if self.expanded {
NSLayoutConstraint.activate([self.height].compactMap { $0 })
} else {
NSLayoutConstraint.deactivate([self.height].compactMap { $0 })
}
self.layoutIfNeeded()
})
expanded.toggle()
}
The full code of this example is available on my GitHub Repo: ScrollAnimationExample
Reviewing your GitHub repo...
The issue is due to the view that is animated. You want to run .animate() on the "top-most" view in the hierarchy.
To do this, you can either create a new property of your ExpandableView, such as:
var topMostView: UIView?
and then set that property from your view controller, or...
To keep your class encapsulated, let it find the top-most view. Replace your toggle() func with:
#IBAction func toggle() {
// we need to run .animate() on the "top" superview
// make sure we have a superview
guard self.superview != nil else {
return
}
// find the top-most superview
var mv: UIView = self
while let s = mv.superview {
mv = s
}
// UITextView has subviews, one of which is a _UITextContainerView,
// which also has a _UITextCanvasView subview.
// If scrolling is disabled, and the TextView's height is animated to Zero,
// the CanvasView's height is instantly set to Zero -- so it disappears instead of animating.
// So, when the view is "expanded" we need to first enable scrolling,
// and then animate the height (to Zero)
// When the view is NOT expanded, we first disable scrolling
// and then animate the height (to its intrinsic content height)
if expanded {
textView.isScrollEnabled = true
NSLayoutConstraint.activate([height].compactMap { $0 })
} else {
textView.isScrollEnabled = false
NSLayoutConstraint.deactivate([height].compactMap { $0 })
}
UIView.animate(withDuration: 0.6, animations: {
mv.layoutIfNeeded()
})
expanded.toggle()
}

Reset offset, onTapGesture works, but onRotated does not work, why?

I want the view containing 2 rectangles floating in the bottom of the screen, whatever the orientation is portrait or landscape.
code is a test, when orientationDidChangeNotification happened, I Found UIDevice.current.orientation.isPortrait and UIScreen.main.bounds.height often have wrong value, why?
Anyway, test code is just reset offset = 0 in onRotated(). but it doesn't work; otherwise onTapGesture works fine.
Q1: Is it a wrong way for SwiftUI? SceneDelegate.orientationDidChangeNotification -> contentView.onRotated()?
Q2: why do UIDevice.current.orientation.isPortrait and UIScreen.main.bounds.height often have wrong value?
Q3: How to let a view float at the bottom of screen in both portrait and landscape?
let height: CGFloat = 100
struct TestView: View {
#State var offset = (UIScreen.main.bounds.height - height) / 2
var body: some View {
ZStack {
Text("+")
VStack(spacing: 0) {
Rectangle().fill(Color.blue)
Rectangle().fill(Color.red)
}
.frame(width: 100, height: height)
.offset(y: offset)
.onTapGesture {
self.offset = 0
}
}
}
func onRotated() {
// let isPortrait = UIDevice.current.orientation.isPortrait
offset = 0//(UIScreen.main.bounds.height - height) / 2
// print("\(isPortrait), screen height = \(UIScreen.main.bounds.height)")
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = TestView()
if let windowScene = scene as? UIWindowScene {
NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { notification in
contentView.onRotated()
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
...
}
contentView is not a reference, it is a value, so you call .onRotated on own copy of contentView value that lives only within callback
let contentView = TestView()
if let windowScene = scene as? UIWindowScene {
NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { notification in
contentView.onRotated() // local copy!!!
}
instead create listener for notification publisher inside TestView, so it can change self internally.
Moreover, it is not clear the intention but SwiftUI gives possibility to track size classes via EnvironmentValues.horizontalSizeClass and EnvironmentValues.verticalSizeClass which are automatically changed on device orientation, so it is possible to make your view layout depending on those environment values even w/o notification.
See here good example on how to use size classes

NSDocument printOperationWithSettings not showing all pages

In NSDocument subclass, have this function:
override func printOperationWithSettings(printSettings: [String : AnyObject]) throws -> NSPrintOperation {
let printInfo: NSPrintInfo = self.printInfo
var pageSize = printInfo.paperSize
pageSize.width -= printInfo.leftMargin + printInfo.rightMargin
pageSize.height -= printInfo.topMargin + printInfo.bottomMargin
pageSize.width = pageSize.width * 2
pageSize.height = pageSize.height * 2
let myPage = MyPage(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: pageSize))
let printOperation = NSPrintOperation(view: myPage, printInfo: printInfo)
return printOperation
}
MyPage is, for this test, an NSView subclass that just draws an oval.
class MyPage: NSView {
override var flipped: Bool {
return true
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.greenColor().set() // choose color
let figure = NSBezierPath() // container for line(s)
figure.appendBezierPathWithOvalInRect(self.frame)
figure.stroke() // draw line(s)
}
}
I'd expect this to show four pages in the print panel, but it only shows two, equating to the top left and bottom left of the oval. No matter how wide I make myPage's frame, only the leftmost pages are shown. Any ideas why? Thank you!

Dynamically adding cells to a NSMatrix laid out with Auto Layout has weird effects; why?

I want to create a group of radio buttons using the NSMatrix method that Interface Builder uses, but in code. The matrix is laid out using Auto Layout. I have it mostly working, except for when I add new options at runtime.
In the following example, clicking Append Item a few times will work fine, then the matrix starts going out of the window near the top (at least I think it's clipped at the top). If you maximize this window after adding a bunch of items, the window will stay the same height and all the items will be clipped to about a pixel high each, which is a very undesirable thing :)
In my real program (not this test below), it works mostly fine, but if I add an option dynamically, after certain numbers of items (initially 5), the options will clip very slightly, appearing slightly squeezed or squished. Adding another option reverts this until the next magic number is hit.
What's going on? I'm testing this on OS X Yosemite. Thanks.
// 17 august 2015
import Cocoa
var keepAliveMainwin: NSWindow? = nil
var matrix: NSMatrix? = nil
class ButtonHandler : NSObject {
#IBAction func onClicked(sender: AnyObject) {
var lastRow = matrix!.numberOfRows
matrix!.renewRows(lastRow + 1, columns: 1)
var cell = matrix!.cellAtRow(lastRow, column: 0) as! NSButtonCell
cell.title = "New Item"
matrix!.sizeToCells()
}
}
var buttonHandler: ButtonHandler = ButtonHandler()
func appLaunched() {
var mainwin = NSWindow(
contentRect: NSMakeRect(0, 0, 320, 240),
styleMask: (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask),
backing: NSBackingStoreType.Buffered,
defer: true)
var contentView = mainwin.contentView as! NSView
var prototype = NSButtonCell()
prototype.setButtonType(NSButtonType.RadioButton)
prototype.font = NSFont.systemFontOfSize(NSFont.systemFontSizeForControlSize(NSControlSize.RegularControlSize))
matrix = NSMatrix(frame: NSZeroRect,
mode: NSMatrixMode.RadioModeMatrix,
prototype: prototype,
numberOfRows: 0,
numberOfColumns: 0)
matrix!.allowsEmptySelection = false
matrix!.selectionByRect = true
matrix!.intercellSpacing = NSMakeSize(4, 2)
matrix!.autorecalculatesCellSize = true
matrix!.drawsBackground = false
matrix!.drawsCellBackground = false
matrix!.autosizesCells = true
matrix!.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(matrix!)
var button = NSButton(frame: NSZeroRect)
button.title = "Append Item"
button.setButtonType(NSButtonType.MomentaryPushInButton)
button.bordered = true
button.bezelStyle = NSBezelStyle.RoundedBezelStyle
button.font = NSFont.systemFontOfSize(NSFont.systemFontSizeForControlSize(NSControlSize.RegularControlSize))
button.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(button)
button.target = buttonHandler
button.action = "onClicked:"
var views: [String: NSView]
views = [
"button": button,
"matrix": matrix!,
]
addConstraints(contentView, "V:|-[matrix]-[button]-|", views)
addConstraints(contentView, "H:|-[matrix]-|", views)
addConstraints(contentView, "H:|-[button]-|", views)
mainwin.cascadeTopLeftFromPoint(NSMakePoint(20, 20))
mainwin.makeKeyAndOrderFront(mainwin)
keepAliveMainwin = mainwin
}
func addConstraints(view: NSView, constraint: String, views: [String: NSView]) {
var constraints = NSLayoutConstraint.constraintsWithVisualFormat(
constraint,
options: NSLayoutFormatOptions(0),
metrics: nil,
views: views)
view.addConstraints(constraints)
}
class appDelegate : NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(note: NSNotification) {
appLaunched()
}
func applicationShouldTerminateAfterLastWindowClosed(app: NSApplication) -> Bool {
return true
}
}
func main() {
var app = NSApplication.sharedApplication()
app.setActivationPolicy(NSApplicationActivationPolicy.Regular)
// NSApplication.delegate is weak; if we don't use the temporary variable, the delegate will die before it's used
var delegate = appDelegate()
app.delegate = delegate
app.run()
}
main()
Apparently, you need to omit the call to sizeToCells() after calling renewRows(_:columns:). My guess is that it sets the frame size, which is mostly useless when using auto layout, but also clears a "dirty" flag somewhere that tells the matrix that it needs to invalidate its intrinsic size. In other words, the matrix thinks it already did the re-layout stuff it needed to do.

iOS 8 Swift Xcode 6 - Set top nav bar bg color and height

I have looked everywhere and tested all the code snippets posted on Stack, but nothing works for me as I need it to work.
I simply want to set:
Nav bar height
Nav bar bg color in RGB
Nav bar centered logo
I'm working with iOS8, Xcode 6 and Swift.
Many thanks for a clear answer!
This is my code in ViewController.swift
// Set nav bar height
navigationController?.navigationBar.frame.origin.y = -10
// Set nav bar bg color
var navBarColor = UIColor(red: 4 / 255, green: 47 / 255, blue: 66 / 255, alpha: 1)
navigationController?.navigationBar.barTintColor = navBarColor
// Set nav bar logo
let navBarImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
navBarImageView.contentMode = .ScaleAspectFit
let navBarImage = UIImage(named: "navBarLogo.png")
navBarImageView.image = navBarImage
navigationItem.titleView = navBarImageView
After applying the code in the accepted answer, the height doesn't seem to change at all..
It's not an easy job...and I've surveyed several articles online (most of them in Objective-C).
The most useful one is this: http://www.emdentec.com/blog/2014/2/25/hacking-uinavigationbar
But its final solution does not put items in the middle, and it's not in Swift.
So I come up with a workable version in Swift. Hope it helps some people as I was saved so many precious time on SO.
Solution in Swift:
The following code will solve some issues you may have encountered:
The title & items are not placed in the middle of the navigation bar
The title & items would flick when the user navigates between view controllers
You need to subclass the UINavigationBar first, and in your storyboard, select the navigation bar element, and in the "Identity Inspector" tab, set the new class as the Custom Class
import UIKit
class UINavigationBarTaller: UINavigationBar {
///The height you want your navigation bar to be of
static let navigationBarHeight: CGFloat = 64
///The difference between new height and default height
static let heightIncrease:CGFloat = navigationBarHeight - 44
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
let shift = UINavigationBarTaller.heightIncrease/2
///Transform all view to shift upward for [shift] point
self.transform =
CGAffineTransformMakeTranslation(0, -shift)
}
override func layoutSubviews() {
super.layoutSubviews()
let shift = UINavigationBarTaller.heightIncrease/2
///Move the background down for [shift] point
let classNamesToReposition: [String] = ["_UINavigationBarBackground"]
for view: UIView in self.subviews {
if classNamesToReposition.contains(NSStringFromClass(view.dynamicType)) {
let bounds: CGRect = self.bounds
var frame: CGRect = view.frame
frame.origin.y = bounds.origin.y + shift - 20.0
frame.size.height = bounds.size.height + 20.0
view.frame = frame
}
}
}
override func sizeThatFits(size: CGSize) -> CGSize {
let amendedSize:CGSize = super.sizeThatFits(size)
let newSize:CGSize = CGSizeMake(amendedSize.width, UINavigationBarTaller.navigationBarHeight);
return newSize;
}
}
Also on my gist: https://gist.github.com/pai911/8fa123d4068b61ad0ff7
iOS 10 Update:
Unfortunately, this code breaks in iOS 10, there is someone who helps fix it, here you go:
iOS 10 custom navigation bar height
And to be clear, this code is kind of hacky since it depends on the navigation bar's internal structure...so if you decide to use it anyway, be prepared for any upcoming changes that may break this code...
Nav bar height:
In a custom navigation controller subclass...
The trick with this one is to NOT change the actual height of the navigation bar and instead adjust its origin.
func viewDidLoad() {
super.viewDidLoad()
navigationBar.frame.origin.y = -10
}
Nav bar bg color in RGB:
In a custom navigation controller subclass...
func viewDidLoad() {
super.viewDidLoad()
navigationBar.barTintColor = // YOUR COLOR
}
or use the appearance proxy
UINavigationBar.appearance().barTintColor = // YOUR COLOR
Nav bar centered logo
In a custom view controller...
func viewDidLoad() {
super.viewDidLoad()
navigationItem.titleView = UIImageView(image: // YOUR LOGO)
}
Great answer from Bon Bon!
In Swift 3 however make sure you replace
let classNamesToReposition: [String] = ["_UINavigationBarBackground"]
with
let classNamesToReposition: [ String ] = [ "_UIBarBackground" ]
Otherwise, it wont work.

Resources