Stack view bug or did I miss something? - ios

Written a sample app here to illustrate some animation behaviour that doesn't seem quite right? I create a stack view in the lower half of the screen, add a button to it, animate it moving it up the screen {it moves} including the button, but the action attached to the button remains in the lower half?
Indeed if I look at the debugger on Xcode, it hasn't moved at all.
There is some action I missed here? yes?
import UIKit
class ViewController: UIViewController {
var selectSV: UIStackView!
var goNorth:CABasicAnimation!
override func viewDidLoad() {
super.viewDidLoad()
selectSV = UIStackView(frame: CGRect(x: 256, y: 696, width: 256, height: 196))
selectSV!.axis = .Vertical
selectSV!.spacing = 4.0
selectSV!.alignment = .Center
selectSV!.distribution = .FillEqually
let zeroRect = CGRect(x: 0,y: 0,width: 0,height: 0)
let newB = UIButton(frame: zeroRect)
newB.setTitle("Blah", forState: UIControlState.Normal)
newB.titleLabel!.font = UIFont(name: "Palatino", size: 24)
newB.frame = CGRect(x: 0, y: 0, width: 128, height: 32)
newB.addTarget(self, action: #selector(ViewController.blahblah(_:)), forControlEvents: .TouchUpInside)
newB.backgroundColor = UIColor.blueColor()
self.selectSV.addArrangedSubview(newB)
self.view.addSubview(selectSV)
NSTimer.scheduledTimerWithTimeInterval(4.0, target: self, selector: #selector(ViewController.moveSV), userInfo: nil, repeats: false)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func blahblah(sender: AnyObject) {
print("You hit me!")
}
func moveSV() {
self.animateNorth()
self.selectSV.layer.addAnimation(self.goNorth, forKey: nil)
}
func animateNorth() {
goNorth = CABasicAnimation(keyPath: "position.y")
goNorth.fromValue = 792
goNorth.toValue = 288
goNorth.duration = 4.0
goNorth.delegate = self
goNorth.setValue("window", forKey:"goNorth")
goNorth.removedOnCompletion = false
goNorth.fillMode = kCAFillModeForwards
}
}

You are animating the backing layer of your stackview and not the frame of the stackview itself. To animate the stackview you could use the UIView method animateWithDuration:animations: and animate the frame property of the stackview. To see better what happens now, you could enable selectSV.clipsToBounds = true to your stackview. That way the layer of the stackview is clipped when it gets out of the frame.

Thanks Jelly!
Commented out this ...
self.animateNorth()
self.selectSV.layer.addAnimation(self.goNorth, forKey: nil)
and replaced it with this works perfectly!!
UIView.animateWithDuration(0.5, animations: {
self.selectSV.center.y = 288.0
})
Easy when you know how... much to learn in this domain...

Related

Animate image crop from right to left

I want to animate a change in the width of an ImageView in Swift, in a way it will be appeared as if the image is being cropped from right to left. I mean that I want the image to always stick to the left edge of its superView and only its right edge will be changed during the animation, without changing the image scale.
I've managed to animate the change in the width of the image while preserving its scale, but the image is being cropped from both sides towards its centerX and not from right to left only.
imageView.contentMode = .scaleAspectFill
imageView.clipToBounds = true
let animation = CABasicAnimation(keyPath: "bounds.size.width")
animation.fromValue = 250
animation.toValue = 50
imageView.layer.add(animation, forKey: nil)
According to this SO thread one should change the anchorPoint of imageView.layer to have x=0, but doing so moves the left edge of the image to the center of the view, and also moves the image when animating so that when it is in its smaller width, the image's centerX point will be visible at the center of the screen.
I would suggest you to use additional view (panel) that defines the size and place your image view on this view. Panel may then clip the image view to create a desired effect.
I created a short example all in code just to demonstrate the approach:
class ViewController: UIViewController {
private var isImageExtended: Bool = true
private var imagePanel: UIView?
override func viewDidLoad() {
super.viewDidLoad()
let panel = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 100.0))
panel.backgroundColor = .lightGray
let imageView = UIImageView(image: UIImage(named: "test_image"))
imageView.frame = CGRect(x: 0.0, y: 0.0, width: panel.bounds.width, height: panel.bounds.height)
imageView.contentMode = .scaleAspectFill
imageView.translatesAutoresizingMaskIntoConstraints = false
panel.addSubview(imageView)
imageView.backgroundColor = .red
view.addSubview(panel)
panel.center = view.center
panel.clipsToBounds = true
imagePanel = panel
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
#objc private func onTap() {
guard let imagePanel else { return }
isImageExtended = !isImageExtended
let panelWidth: CGFloat = isImageExtended ? 200 : 50
UIView.animate(withDuration: 0.5) {
imagePanel.frame.size.width = panelWidth
}
}
}
I could easily achieve the same result using storyboard and constraints with significantly less code:
class ViewController: UIViewController {
#IBOutlet private var panelWidthConstrain: NSLayoutConstraint?
private var isImageExtended: Bool = true
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
#objc private func onTap() {
isImageExtended = !isImageExtended
let panelWidth: CGFloat = isImageExtended ? 200 : 50
UIView.animate(withDuration: 0.5) {
self.panelWidthConstrain?.constant = panelWidth
self.view.layoutIfNeeded()
}
}
}
I hope the code speaks enough for itself.

Adding GestureRecognizer to UIView won't trigger action

I have no idea why this gesture recognizer is not working as intended:
class SlideInMenuLauncher: NSObject, UITableViewDelegate, UITableViewDataSource {
fileprivate let dimmerView = UIView()
fileprivate let tableView: UITableView = {
let tbv = UITableView(frame: .zero)
return tbv
}()
fileprivate let resourceArray: [Content]
weak var delegate: SlideInMenuDelegate?
let cellIdentifier = "SlideInMenuTableViewCell"
init(withContentArray contentArray: [Content]) {
resourceArray = contentArray
super.init()
tableView.dataSource = self
tableView.delegate = self
tableView.register(UINib(nibName: cellIdentifier, bundle: nil), forCellReuseIdentifier: cellIdentifier)
}
func showMenu() {
if let window = UIApplication.shared.keyWindow {
dimmerView.backgroundColor = UIColor(white: 0, alpha: 0.5)
dimmerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissMenu)))
dimmerView.isUserInteractionEnabled = true
window.addSubview(dimmerView)
window.addSubview(tableView)
let height: CGFloat = 200
let y = window.frame.height - height
tableView.frame = CGRect(x: 0, y: window.frame.height, width: window.frame.width, height: height)
dimmerView.frame = window.frame
dimmerView.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.dimmerView.alpha = 1
self.tableView.frame = CGRect(x: 0, y: y, width: self.tableView.frame.width, height: self.tableView.frame.height)
}, completion: nil)
}
}
func dismissMenu() {
UIView.animate(withDuration: 0.5) {
self.dimmerView.alpha = 0
if let window = UIApplication.shared.keyWindow {
self.tableView.frame = CGRect(x: 0, y: window.frame.height, width: self.tableView.frame.width, height: self.tableView.frame.height)
}
}
}
I've put a breakpoint in the dismissMenu function but it is never triggered. Perhaps my tired eyes (and mind) missed something simple?
Here I am adding the code where I actually call this class in case something is wrong there:
///Some other VC
let slideInMenuLauncher = SlideInMenuLauncher(withContentArray: [content])
slideInMenuLauncher.showMenu()
I would expect that to give a compiler error. You're missing the #objc declaration on your dismissMenu() function.
I added a view to a view controller in a storyboard and used a small variant of your code and it responds to taps just fine. Thus my guess is that there's something wrong with the way you're adding your views to the view controller.
EDIT:
I know what the problem is: If you set a view's alpha to 0 it stops responding to taps. Try setting the view's opaque flag to false and setting it's background color to clearColor.
Here's the code from the test project I created:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tapView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
tapView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
tapView.isUserInteractionEnabled = true
}
#objc func handleTap() {
print("You tapped on the view")
}
}
Can you try instead of this:
window.addSubview(dimmerView)
window.addSubview(tableView)
Do this:
window.addSubview(dimmerView)
window.addSubview(tableView)
window.bringSubviewToFront(dimmerView)
Try use following code:
let tapGes = UITapGestureRecognizer(target: self, action:#selector(dismissMenu))
tapGes.delegate = self
dimmerView.addGestureRecognizer(tapGes)
Hope it help you!
I think your problem is here:
dimmerView.alpha = 0
If I remember correctly, views that have their alpha set to 0 don't get touches. Try setting is to something like 0.5 just to see if it works, and then dial it back to a smaller value that's still greater than 0.
So I found the problem thanks to this post. When I was using the slideInMenuLauncher class elsewhere, I was not saving it to a strong reference or anything, so once I moved out of the scope of the function where I created the menu, the SlideInMenuLauncher object I used to create the dimmerView and tableView disappeared (despite still being able to see the views). Once I tapped the dimmerView the gesture recognizer sent a message to an object that no longer existed.
Setting the object to a strong property fixed the issue.

How to place a subview in the center of parent view using .center?

I have two subviews: the purple one belongs to a main view and it works wonderful with the syntax:
sSubview.center = view.center
The orange one belongs to the purple view and the same syntax doesn't place it correctly in the center.
Could you see where my mistake is?
Here is my code:
import UIKit
class ViewController: UIViewController {
var sSubview = UIView()
var sLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = UIColor.lightGray
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidLayoutSubviews() {
createSimplestSubview()
}
// First subview
func createSimplestSubview() {
sSubview = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width * 0.9, height: view.frame.height * 0.9))
sSubview.backgroundColor = UIColor.purple // just for test, to make it visible
sSubview.center = view.center // that s pretty easy!
view.addSubview(sSubview)
createSimplestLabel()
}
// Second subview
func createSimplestLabel() {
sLabel = UILabel(frame: CGRect(x: 0, y: 0, width: sSubview.frame.width * 0.3, height: sSubview.frame.height * 0.2))
sLabel.backgroundColor = UIColor.orange // just for test, to make it visible
sLabel.center = sSubview.center
sLabel.text = "This label has to be centered!"
sLabel.textAlignment = .center
sLabel.numberOfLines = 0
sSubview.addSubview(sLabel)
}
}
Here is a screen:
Try the next code for seconds subview:
sLabel.center = CGPoint(x: sSubview.frame.width / 2, y: sSubview.frame.height / 2)
You're trying to place a label in the center, but it calculated according to the firstSbuview frame, which origin is not 0.0
A better way is to use convertPoint as the problem with your code is that the coordinate spaces are sometimes different.
sSubview.center = view.superview.convert(view.center, to: sSubview.superview)
view.center is in the coordinate space of view.superview
sSubview.center is in the coordinate space of sSubview.superview

Unable to change contentSize of UIScrollview in WKWebView

I'm trying to add a little bit of extra height to the content of a UIScrollView that is within a WKWebView after it loads by adjusting the contentSize property.
I can modify this property, but it somehow keeps changing back to its original size by the time the next layout/display refresh hits.
To test this even further, I attempted to change contentSize in scrollViewDidScroll. Whenever you scroll to the bottom, you can see for a fraction of a second that it's trying add the extra space and keeps reverting back.
I can't reproduce this issue with UIWebView. It works just fine there. Perhaps some changes were added to WKWebView recently? I'm using Xcode 8 and testing on iOS 9/10.
Given my ineptitude with Dropbox I felt badly so put the attached together to try and help you out. If you change the contentInset property of the WKWebView's scrollView rather than contentSize, this seems to work quite well. I agree with you that while you might be able temporarily to change the content size of the scrollView, it reverts quickly; moreover, there are no delegate methods either for UIScrollView or WKWebView that I can find that you might override to counteract this.
The following sample code has a web page and some buttons that allow you to increase or decrease the top and bottom contentInset and animating you to the appropriate point on the scrollView.
import UIKit
import WebKit
class ViewController: UIViewController {
var webView : WKWebView!
var upButton : UIButton!
var downButton : UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let webFrame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: self.view.frame.width - 200, height: self.view.frame.height - 200))
webView = WKWebView(frame: webFrame)
webView.load(URLRequest(url: URL(string: <PUT RELEVANT URL STRING (NB - THAT YOU ARE SURE IS VALID) HERE>)!))
webView.backgroundColor = UIColor.red
webView.scrollView.contentMode = .scaleToFill
self.view.addSubview(webView)
func getButton(_ label: String) -> UIButton {
let b : UIButton = UIButton()
b.setTitle(label, for: .normal)
b.setTitleColor(UIColor.black, for: .normal)
b.layer.borderColor = UIColor.black.cgColor
b.layer.borderWidth = 1.0
return b
}
let upButton = getButton("Up")
let downButton = getButton("Down")
upButton.frame = CGRect(origin: CGPoint(x: 25, y: 25), size: CGSize(width: 50, height: 50))
downButton.frame = CGRect(origin: CGPoint(x: 25, y: 100), size: CGSize(width: 50, height: 50))
upButton.addTarget(self, action: #selector(increaseContentInset), for: .touchUpInside)
downButton.addTarget(self, action: #selector(decreaseContentInset), for: .touchUpInside)
self.view.addSubview(webView)
self.view.addSubview(upButton)
self.view.addSubview(downButton)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func increaseContentInset() -> Void {
guard let _ = webView else { return }
webView.scrollView.contentInset = UIEdgeInsetsMake(webView.scrollView.contentInset.top + 100, 0, webView.scrollView.contentInset.bottom + 100, 0)
webView.scrollView.setContentOffset(CGPoint(x: webView.scrollView.contentInset.left, y: -1 * webView.scrollView.contentInset.top), animated: true)
}
func decreaseContentInset() -> Void {
guard let _ = webView else { return }
webView.scrollView.contentInset = UIEdgeInsetsMake(webView.scrollView.contentInset.top - 100, 0, webView.scrollView.contentInset.bottom - 100, 0)
webView.scrollView.setContentOffset(CGPoint(x: webView.scrollView.contentInset.left, y: -1 * webView.scrollView.contentInset.top), animated: true)
}
}
I hope that helps. If you need an answer based specifically on setting the content size then let me know, but I think this is the best option.

My UIButtons (using auto layout) seem not be drawn correctly when changing orientation

When I started this viewController, I had in mind the following layout, drawn in the Interface Builder with an auto layout:
Then I thought, why not adding a calculator mode that would switch the buttons 500, 100, 50 and 10 to /, x, - and + with a flip transition.
So as the layout was already defined, I thought I could take the existing buttons (the ones I want to flip), make them as the containers of my transitions and create programmatically the buttons 500, 100, 50, 10, /, x, - and + based on the frame of those containers (in order to avoid to manage the auto layout programmatically.
So I wrote the following code in my viewController (all views are UIButton):
override func viewDidLayoutSubviews() {
initLayout()
}
func initLayout() {
container500.backgroundColor = UIColor.greenColor()
container100.backgroundColor = UIColor.greenColor()
container50.backgroundColor = UIColor.greenColor()
container10.backgroundColor = UIColor.greenColor()
button500 = createCalculatorButton(NSNumberFormatter.localizedStringFromNumber(500, numberStyle: .NoStyle)+"wwwww",
frame: container500.bounds, color: container500.backgroundColor!, action: "fiveHundreds:")
button100 = createCalculatorButton(NSNumberFormatter.localizedStringFromNumber(100, numberStyle: .NoStyle)+"xxxxx",
frame: container100.bounds, color: container100.backgroundColor!, action: "oneHundred:")
button50 = createCalculatorButton(NSNumberFormatter.localizedStringFromNumber(50, numberStyle: .NoStyle)+"yyyyy", frame: container50.bounds, color: container50.backgroundColor!, action: "fifty:")
button10 = createCalculatorButton(NSNumberFormatter.localizedStringFromNumber(10, numberStyle: .NoStyle)+"zzzzz", frame: container10.bounds, color: container10.backgroundColor!, action: "ten:")
buttonDivision = createCalculatorButton("/", frame: container500.bounds, color: UIColor.lightGrayColor(), action: "div:")
buttonMultiplication = createCalculatorButton("x", frame: container100.bounds, color: UIColor.lightGrayColor(), action: "mult:")
buttonSubtraction = createCalculatorButton("-", frame: container50.bounds, color: UIColor.lightGrayColor(), action: "sub:")
buttonAddition = createCalculatorButton("+", frame: container10.bounds, color: UIColor.lightGrayColor(), action: "add:")
container500.addSubview(button500)
container100.addSubview(button100)
container50.addSubview(button50)
container10.addSubview(button10)
}
func createCalculatorButton(title: String, frame: CGRect, color: UIColor, action: Selector) -> UIButton {
var button = UIButton()
button.setTitle(title, forState: .Normal)
button.backgroundColor = color
button.layer.borderWidth = 1
button.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height)
button.addTarget(self, action: action, forControlEvents: .TouchUpInside)
return button
}
#IBAction func calculatorMod(sender: AnyObject) {
self.flipTransition(view1: self.button500, view2: self.buttonDivision)
GraphicHelper.delay(0.1) {
self.flipTransition(view1: self.button100, view2: self.buttonMultiplication)
}
GraphicHelper.delay(0.2) {
self.flipTransition(view1: self.button50, view2: self.buttonSubtraction)
}
GraphicHelper.delay(0.3) {
self.flipTransition(view1: self.button10, view2: self.buttonAddition)
}
}
func flipTransition(#view1: UIView, view2: UIView) {
var transitionOptions: UIViewAnimationOptions!
let duration = 0.5
var views : (frontView: UIView, backView: UIView)
if view1.superview != nil {
views = (frontView: view1, backView: view2)
transitionOptions = UIViewAnimationOptions.TransitionFlipFromLeft
}
else {
views = (frontView: view2, backView: view1)
transitionOptions = UIViewAnimationOptions.TransitionFlipFromRight
}
UIView.transitionFromView(views.frontView, toView: views.backView, duration: duration, options: transitionOptions, completion: nil)
}
And here is the result:
When entering in portrait. OK:
And then switching to landscape. NOT OK:
When entering in portrait and then switching to calculator mode. OK:
When entering in landscape. OK:
And then switching to portrait. NOT OK:
And then switching to Calculator mode. NOT OK:
By seeing this, I can guess the UIButtons before changing the phone's orientation are still there (or they seem to be) but I don't understand where is the problem in my code.
I even tried
override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {
button500.removeFromSuperview()
button100.removeFromSuperview()
button50.removeFromSuperview()
button10.removeFromSuperview()
buttonDivision.removeFromSuperview()
buttonMultiplication.removeFromSuperview()
buttonSubtraction.removeFromSuperview()
buttonAddition.removeFromSuperview()
}
but it doesn't change anything.
Any idea where my problem is?
UPDATE:
Here is the view hierarchy when changing from portrait to landscape:
Thanks.

Resources