Swift LayoutSubviews moving all subviews every user interaction - ios

I apologize if this question has been already addressed, but I cannot find it.
I thought it would be simple enough to animate a UIImageView from offscreen onto the screen at certain points in my program. So I created the UIImageView and coded it up, but am getting a lot of strange behaviour. I have narrowed it down to the fact that layoutSubviews is getting called every user interaction (every button press) and it is resetting the size and location of all my items. I am using auto layout for every screen item save this one, so that is fine for the things I want auto layed out. But this one item is constantly being moved back to it's starting location. I proved it this way:
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad")
let screenWidth = UIScreen.mainScreen().bounds.width
let screenHeight = UIScreen.mainScreen().bounds.height
print(image.center)
image.center = CGPointMake(screenWidth / 2, image.bounds.height / -2)
print(image.center)
}
override func viewWillLayoutSubviews() {
print("viewWillLayoutSubviews")
print(image.center)
}
override func viewDidLayoutSubviews() {
print("viewDidLayoutSubviews")
print(image.center)
}
This is the result I get:
viewDidLoad
(294.0, 228.0)
(160.0, -64.0)
viewWillLayoutSubviews
(160.0, -64.0)
viewDidLayoutSubviews
(294.0, 228.0)
I don't have any constraints attached to this one view. Thoughts? I guess I could store the location and constantly move it around manually, but I'm sure there is a switch somewhere that turns off the autolayout for specific items. I just can't find it.
Thanks for your help...

The image must have autoresizingMasks that are getting translated into constraints.
Disable it by doing this:
image.translatesAutoresizingMaskIntoConstraints = false

Related

WKWebview goes blank after device rotation

I'm making a scrollview with multiple subviews, includes collectionView, webView, and normal UIview. As far as Ive read through, it is not recommended to put webview inside a scrollview. But it is still ok to add webview as subview inside the scrollview.
The requirement is that the webview will dynamically load the content onClick. So, at first, there was a bit of issue trying to dynamically resize the height of the webview as per the content size. The height gets added extra pixels after every rotation. But somehow managed to make it work using webview.sizeToFit() in the viewDidLayoutSubviews().
But now the problem is that, the content of the webview disappears after every 2 rotation of the device. The content should still be there because the webview's frame size didnt change after roation, just the texts inside disappear.
There is not other error as to why the webview is showing blank for after rotated.This is my viewdidlayoutsubview.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
webView.frame.size.height = 1
webView.frame.size = webView.sizeThatFits(.zero)
webView.sizeToFit()
}
Not sure how to solve.
I will just answer myself here, since the solution last time was still working well for this. Refresh the view layouts on in the override func viewWillTransition. As per apple's doc, "UIKit calls this method before changing the size of a presented view controller's view". So Ive refreshed all the subview's before changing the orientation.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator){
super.viewWillTransition(to: size, with: coordinator)
//code to refresh the views
}
Just add this line below wkwebview code
i named var = webView so,
webView.autoresizingmask = [.flexiblewidth,.flexibleheight]
hope it works.

How to make UIScrollView zoom in only one direction when using auto layout

I'm attempting to make a UIScrollView only allow zooming in the horizontal direction. The scroll view is setup with a pure autolayout approach. The usual approach is as suggested in a number of Stack Overflow answers (e.g. this one) and other resources. I have successfully used this in apps before autolayout existed. The approach is as follows: in the scroll view's content view, I override setTransform(), and modify the passed in transform to only scale in the x direction:
override var transform: CGAffineTransform {
get { return super.transform }
set {
var t = newValue
t.d = 1.0
super.transform = t
}
}
I also reset the scroll view's content offset so that it doesn't scroll vertically during the zoom:
func scrollViewDidZoom(scrollView: UIScrollView) {
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
}
This works very nicely when not using autolayout:
However, when using autolayout, the content offset ends up wrong during zooming. While the content view only scales in the horizontal direction, it moves vertically:
I've put an example project on GitHub (used to make the videos in this question). It contains two storyboards, Main.storyboard, and NoAutolayout.storyboard, which are identical except that Main.storyboard has autolayout turned on while NoAutolayout does not. Switch between them by changing the Main Interface project setting and you can see behavior in both cases.
I'd really rather not switch off autolayout as it solves a number of problems with implementing my UI in a much nicer way than is required with manual layout. Is there a way to keep the vertical content offset correct (that is, zero) during zooming with autolayout?
EDIT: I've added a third storyboard, AutolayoutVariableColumns.storyboard, to the sample project. This adds a text field to change the number of columns in the GridView, which changes its intrinsic content size. This more closely shows the behavior I need in the real app that prompted this question.
Think I figured it out. You need to apply a translation in the transform to offset the centering UIScrollView tries to do while zooming.
Add this to your GridView:
var unzoomedViewHeight: CGFloat?
override func layoutSubviews() {
super.layoutSubviews()
unzoomedViewHeight = frame.size.height
}
override var transform: CGAffineTransform {
get { return super.transform }
set {
if let unzoomedViewHeight = unzoomedViewHeight {
var t = newValue
t.d = 1.0
t.ty = (1.0 - t.a) * unzoomedViewHeight/2
super.transform = t
}
}
}
To compute the transform, we need to know the unzoomed height of the view. Here, I'm just grabbing the frame size during layoutSubviews() and assuming it contains the unzoomed height. There are probably some edge cases where that's not correct; might be a better place in the view update cycle to calculate the height, but this is the basic idea.
Try setting translatesAutoresizingMaskIntoConstraints
func scrollViewDidZoom(scrollView: UIScrollView) {
gridView.translatesAutoresizingMaskIntoConstraints = true
}
In the sample project it works. If this is not sufficient, try creating new constraints programmatically in scrollViewDidEndZooming: might help.
Also, if this does not help, please update the sample project so we can reproduce the problem with variable intrinsicContentSize()
This article by Ole Begemann helped me a lot How I Learned to Stop Worrying and Love Cocoa Auto Layout
WWDC 2015 video
Mysteries of Auto Layout, Part 1
Mysteries of Auto Layout, Part 2
And so luckily, there's a flag for that. It's called translatesAutoResizingMask IntoConstraints [without space].
It's a bit of a mouthful, but it pretty much does what it says. It makes views behave the way that they did under the Legacy Layout system but in an Auto Layout world.
Although #Anna's solution works, UIScrollView provides a way of working with Auto Layout. But because scroll views works a little differently from other views, constraints are interpreted differently too:
Constraints between the edges/margins of scroll view and its contents attaches to the scroll view's content area.
Constraints between the height, width, or centers attach to the scroll view’s frame.
Constraints between scroll view and views outside scroll view works like an ordinary view.
So, when you add a subview to the scroll view pinned to its edges/margins, that subview becomes the scroll view's content area or content view.
Apple suggests the following approach in Working with Scroll Views:
Add the scroll view to the scene.
Draw constraints to define the scroll view’s size and position, as normal.
Add a view to the scroll view. Set the view’s Xcode specific label to Content View.
Pin the content view’s top, bottom, leading, and trailing edges to the scroll view’s corresponding edges. The content view now defines
the scroll view’s content area.
(...)
(...)
Lay out the scroll view’s content inside the content view. Use constraints to position the content inside the content view as normal.
In your case, the GridView must be inside the content view:
You can keep the grid view constraints that you have been using but, now, attached to content view. And for the horizontal-only zooming, keep the code exactly as it is. Your transform overriding handle it very well.
I'm not sure if this satisfies your requirement of 100% autolayout, but here's one solution, where the UIScrollView is set with autolayout and gridView added as a subview to it.
Your ViewController.swift should look like this:
// Add an outlet for the scrollView in interface builder.
#IBOutlet weak var scrollView: UIScrollView!
// Remove the outlet and view for gridView.
//#IBOutlet var gridView: GridView!
// Create the gridView here:
var gridView: GridView!
// Setup some views
override func viewDidLoad() {
super.viewDidLoad()
// The width for the gridView is set to 1000 here, change if needed.
gridView = GridView(frame: CGRect(x: 0, y: 0, width: 1000, height: self.view.bounds.height))
scrollView.addSubview(gridView)
scrollView.contentSize = CGSize(width: gridView.frame.width, height: scrollView.frame.height)
gridView.contentMode = .ScaleAspectFill
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return gridView
}
func scrollViewDidZoom(scrollView: UIScrollView) {
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
}

Swift: Unexpected zoom in ad

I added the MoPub banner ad with the following code:
self.adView.delegate = self
self.adView.frame = CGRectMake(0, self.view.bounds.size.height - MOPUB_BANNER_SIZE.height,
screenWidth, MOPUB_BANNER_SIZE.height)
self.view.addSubview(self.adView)
self.adView.loadAd()
For some reason when I run the application the screen doesn't appear fully, but it has a weird zoom. What could be happening?
UPDATE:
I moved the frame of the adView to viewDidLayoutSubviews() like this:
override func viewDidLoad(){
self.adView.delegate = self
self.view.addSubview(self.adView)
self.adView.loadAd()
}
override func viewDidLayoutSubviews() {
var screenSize: CGRect = UIScreen.mainScreen().bounds
var screenWidth = screenSize.width
self.adView.frame = CGRectMake(0, self.view.bounds.size.height - MOPUB_BANNER_SIZE.height,
MOPUB_BANNER_SIZE.width, MOPUB_BANNER_SIZE.height)
}
But the error persists, I'm using Auto Layout and Size Classes.
The frame properties that are present in viewDidLoad do not reflect the actual frame properties of the view that will be presented on screen. In this method the outlets have been created and all that, but not layed out yet.
Therefore it is the correct place to create an AdView, setting its properties...
BUT the layout has to be done somewhere else - the best place is viewDidLayoutSubviews. If this method is called you can be sure that the frame properties are set correctly.
Before changing anything in your code you can just log the frame in viewDidLoad and log them in viewDidLayoutSubviews - they should be different. Then you can go ahead and move your layout logic over there as well.
I fixed this issue by myself. When working with Spritekit and scenes, remember to always configure the scene before presenting it, specially the scene's ScaleMode. After setting the scene's scaleMode to: scaleMode= .Fill the zoom stopped happening.

custom tableView's header view overlap the table view cells

I designed a custom view as my UITableView's header view. just like this
(I just put image link here instead of image since I don't have 10 reputations.)
http://i.stack.imgur.com/KhNbE.png
Then in my UITableViewController I use this view as tableHeaderView
override func viewDidLoad() {
tableView.tableHeaderView = headerView!
//...other things
}
I got text from a JSON to fulfill the ContentLabel. If the text is long, the headerView will overlap cells just like below image.(short text is OK)
http://i.stack.imgur.com/gtO2g.png
Section is visible but two lines of cell have been overlapped by the headerView.I'm not sure if I did wrong constraints or code on ContentLabel. Below is the code I configured the contentLabel in TopicHeaderView.swift
var content: String? {
didSet {
self.contentLabel.text = content!
self.setNeedsUpdateConstraints()
self.updateConstraintsIfNeeded()
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
func setFrameHeight(height: CGFloat) {
var frame = self.frame
frame.size.height = height
self.frame = frame
}
override func layoutSubviews() {
super.layoutSubviews()
self.contentLabel.preferredMaxLayoutWidth = self.contentLabel.alignmentRectForFrame(contentLabel.frame).width
self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.bounds.size.width
self.authorLabel.preferredMaxLayoutWidth = self.authorLabel.bounds.size.width
self.setFrameHeight(CGRectGetMaxY(contentLabel.frame) + 8)
}
I browsed similar questions in SO but seems I can't find a solution to fix my problem. Can anyone help on this?
EDITED:
I logged the origin CGPoint of my first tableView cell and headerView's height. It shows the right number which means the first cell is right next to the header view vertically. There is a 22 points gap because of the height of section of course.
headerheight:600.0
first cell's y: 622.0
So maybe it's the label problem that its height is too big to exceed the bounds of TableView headerView? I'm not sure.
EDITED:
Strange things happen. I logged the y value of headerView's bottom,contentLabel's bottom and first UITableViewCell's origin. Please see the image from the link in the question comment below(still need 10 reputation)
As you can see, from the value in console, the view sequence from top should be "contentLabel's bottom(value:224) - headerView's bottom bounds(value: 232) - first cell's origin(value:254)". But in simulator, the sequence is totally messed up.It turns "headerView's bottom bounds - first cell's origin - contentLabel's bottom"
I really appreciate if anyone can help on this.
Problem is, that UITableView does not automatically change positions of cells when its headerView's height changes. Thus you need to reload UITableView every time TopicHeaderView.content changes.
Select that header view, or imageView what you have there, and check Clip Subviews in Attributes Inspector tab.
This worked for me.

ScrollView contentOffset reset after pushViewController

I'm experiencing a problem with ScrollView, my scrollView has 640 of width, from 0 to 320 it's a mapView, from 320 to 640 it's a tableView.
I have a segmentedButton with 2 options to switch my view from contentOffset from 0 to 320 in x.
The problem is, if I switch to contentOffset of 320 in x and press a cell of tableView, after releasing the button to push other viewController, the contentOffset of my scrollView is reseting to 0 in x. I need to keep contentOffset on the tableView, at 320 in x.
I tried many things, like playing around with viewWillDisappear and the lifecycle methods.
The closest I've been able to reach was making it going back to the tableView after pressing back. Although, I couldn't make it stay on the tableView to push the next viewController.
Any help is appreciated!
(Ps: I've search for similar questions, but the only one that could help, I couldn't understand very well, the others have similar but different problems).
Thanks
I had the same problem.
Here is the solution:
1. I'm doing content offset backup in viewWillDisappear
2. Restoring offset without animation in viewWillAppear.
UPDATE: Be sure you are not adjusting contentSize in viewWillLayoutSubviews or in methods mentioned above.
I have very similar problem with UItableView which is also UIScrollView, but vertical.
Playing with viewWillDisappear: does not work because there is a noticeable scroll made by iOS when user initiated a push of another VC.
Can't resolve this problem as well.
Saving contentOffset backup at viewWillDisappear is not the correct way of handling which make you even worse to handle if you have some views which required show/hide on viewWillAppear.
The best way is saving the contentOffset at scrollViewDidScroll
Declare the variable where you want to save offset
var previousOffsetY: CGFloat = 0.0
var offsetLimitation: CGFloat = 50.0 // is the limit where I want to play show/hide
Inside UIScrollViewDelegate which is scrollViewDidScroll, save the offset.
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
previousOffsetY = scrollView.contentOffset.y
UIView.animate(withDuration: 0.5, animations: {
if offset <= self.offsetLimitation {
// show
} else {
// hide
}
})
}
}
At viewWillAppear, control the UI you want when offset is reseted.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if previousOffsetY > offsetLimitation {
// hide
}
}
check your scrollview frame when push to a new controller, if scrollview frame changed, scrollview will reset it's contentOffset

Resources