I have a class with...
override func viewDidLayoutSubviews()
{
showDefaultCustomProcessing()
}
On one view, the user can tap and change an image...
#IBAction func changeDoorTap(t:UITapGestureRecognizer)
{
thing.image = UIImage(named:...new image...)
}
It turns out, whenever you change .image, it does a relayout of the screen.
(I did not know that, but it makes sense.)
So unfortunately viewDidLayoutSubviews runs and the CustomProcessing is returned to the default state. That's no good.
As a workaround I did this ........
var requestCustomLayoutAfterViewDidLayoutSubviews:Bool = false
#IBAction func changeDoorTap(t:UITapGestureRecognizer)
{
saveTemporaryCustomProcessing()
thing.image = UIImage(named:...new image...)
requestCustomLayoutAfterViewDidLayoutSubviews = true;
}
override func viewDidLayoutSubviews()
{
performDefaultCustomProcessing()
if (requestCustomLayoutAfterViewDidLayoutSubviews)
{
recoverTemporaryCustomProcessing()
}
requestCustomLayoutAfterViewDidLayoutSubviews = false;
}
Using such a flag is a poor pattern.
1) Is there a way to change a .image but force no relayout?
(Perhaps, by manipulating it at the layer level, or something?)
2) Can you perhaps "turn off" re-lay-out-ing and then turn it on again?
Is there a way to change a .image but force no relayout
Sure: give the UIImageView a width constraint and a height constraint. Without these, it adopts the size of its image. With them, it just obeys the constraints.
Related
I have messages screen and implement custom tableviewcell for the display message. A message should be text or image and some case I need to display boxes with information(see image sender and receiver). it's working fine but some time messages view cut off(see image messages). I have used many stackViews to hiding and show some views.
Please find the code here for more understanding.
The possible cause for such a behaviour is setting up the layers of the view in cell, I can see in your cell, you are adding the corner radius to the background. I could be able to fix it in my app by using the following approach.
Define a optional data varibale in your cell.
var currentData: MessageModel?
set that value in the method you are calling to provide the data to cell.
func loadData(_ data:MessageModel) -> Void {
currentData = data
// YOUR EXISTING CODE GOES HERE.
// Move your code to the function which do the setup of corner radius.
// Call this method.
setupCornerRadius()
}
Add the following methods to your cells
open override func layoutIfNeeded() {
super.layoutIfNeeded()
setupCornerRadius()
}
open override func layoutSubviews() {
super.layoutSubviews()
setupCornerRadius()
}
func setupCornerRadius() {
if let data = currentData {
let strMsg = data.body ?? ""
lblMsgBody.text = strMsg
if strMsg != "" {
viewBG.backgroundColor = UIColor.primaryGreen
if strMsg.count > 5 {
viewBG.layer.cornerRadius = 18.0
}else{
viewBG.layer.cornerRadius = 12.0
}
}
else{
viewBG.backgroundColor = UIColor.clear
}
}
}
Try the above approach.
Along with that what I did was, I removed the stackView from the cell and managed to implement the required UI by setting up the constraints.
Label with numberOfLines = 0 and setting Leading, Trailing, Bottom and Top constraints. with value = 8 (You can set it as per your margin and spacing you needs.)
Try and share the results.
Use UI debugger and see what exactly is going on.
TL;DR
Need to keep autorotation, but exclude one UIView from autorotating on orientation change, how?
Back story
I need to keep a UIView stationary during the animation accompanied by autorotation (which happens on orientation change). Similar to how the iOS camera app handles the rotation (i.e controls rotate in their place).
Things I've tried
Returning false from shouldAutorotate(), subscribing to UIDeviceOrientationDidChangeNotification, and trying to manually handle the rotation event for each view separately.
Works well if you don't need to change any of your UIViews' places, otherwise it's a pain figuring out where it should end up and how to get it there
Placing a non rotating UIWindow under the main UIWindow, and setting the main UIWindow background colour to clear.
This works well if it's only one item, but I don't want to manage a bunch of UIWindows
Inverse rotation I.e rotating the UIView in the opposite direction to the rotation. Not reliable, and looks weird, it's also vertigo inducing
Overriding the animation in the viewWillTransitionToSize method. Failed
And a bunch of other things that would be difficult to list here, but they all failed.
Question
Can this be done? if so, how?
I'm supporting iOS8+
Update This is how the views should layout/orient given #Casey's example:
I have faced with same problem and found example from Apple, which helps to prevent UIView from rotation: https://developer.apple.com/library/ios/qa/qa1890/_index.html
However, if UIView is not placed in the center of the screen, you should handle new position manually.
i think part of the reason this is so hard to answer is because in practice it doesn't really make sense.
say i make a view that uses autolayout to look like this in portrait and landscape:
if you wanted to prevent c from rotating like you are asking, what would you expect the final view to look like? would it be one of these 3 options?
without graphics of the portrait/landscape view you are trying to achieve and a description of the animation you are hoping for it'll be very hard to answer your question.
are you using NSLayoutConstraint, storyboard or frame based math to layout your views? any code you can provide would be great too
If you're wanting to have the same effect as the camera app, use size classes (see here and here).
If not, what is wrong with creating a UIWindow containing a view controller that doesn't rotate? The following code seems to work for me (where the UILabel represents the view you don't want to rotate).
class ViewController: UIViewController {
var staticWindow: UIWindow!
override func viewDidLoad() {
super.viewDidLoad()
showWindow()
}
func showWindow() {
let frame = CGRect(x: 10, y: 10, width: 100, height: 100)
let vc = MyViewController()
let label = UILabel(frame: frame)
label.text = "Hi there"
vc.view.addSubview(label)
staticWindow = UIWindow(frame: frame)
staticWindow.rootViewController = MyViewController()
staticWindow.windowLevel = UIWindowLevelAlert + 1;
staticWindow.makeKeyAndVisible()
staticWindow.rootViewController?.presentViewController(vc, animated: false, completion: nil)
}
}
class MyViewController: UIViewController {
override func shouldAutorotate() -> Bool {
return false
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return false
}
override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
}
How to redraw non-visible UICollectionViewCell's ready for when reuse occurs???
One approach I thought of was per the code in the Layout Cell prepareForReuse function, however whilst it works it non-optimal as it causes more re-drawing then required.
Background: Need to trigger drawRect for cells after an orientation change that are not current visible, but pop up to be used and haven't been redraw, so so far I can only see that prepareForReuse would be appropriate. Issue is I'm re-drawing all "reuse" cells, whereas I really only want to redraw those that initially pop up that were created during the previous orientation position of the device.
ADDITIONAL INFO: So currently I'm doing this:
In ViewController:
override func viewWillLayoutSubviews() {
// Clear cached layout attributes (to ensure new positions are calculated)
(self.cal.collectionViewLayout as! GCCalendarLayout).resetCache()
self.cal.collectionViewLayout.invalidateLayout()
// Trigger cells to redraw themselves (to get new widths etc)
for cell in self.cal?.visibleCells() as! [GCCalendarCell] {
cell.setNeedsDisplay()
}
// Not sure how to "setNeedsDisplay" on non visible cells here?
}
In Layout Cell class:
override func prepareForReuse() {
super.prepareForReuse()
// Ensure "drawRect" is called (only way I could see to handle change in orientation
self.setNeedsDisplay()
// ISSUE: It does this also for subsequent "prepareForReuse" after all
// non-visible cells have been re-used and re-drawn, so really
// not optimal
}
Example of what happens without the code in prepareForReuse above. Snapshot taken after an orientation change, and just after scrolling up a little bit:
I think I have it now here:
import UIKit
#IBDesignable class GCCalendarCell: UICollectionViewCell {
var prevBounds : CGRect?
override func layoutSubviews() {
if let prevBounds = prevBounds {
if !( (prevBounds.width == bounds.width) && (prevBounds.height == bounds.height) ) {
self.setNeedsDisplay()
}
}
}
override func drawRect(rect: CGRect) {
// Do Stuff
self.prevBounds = self.bounds
}
}
Noted this check didn't work in "prepareForReuse" as at this time the cell had not had the rotation applied. Seems to work in "layoutSubviews" however.
You can implement some kind of communication between the cells and the view controller holding the collection view ( protocol and delegate or passed block or even direct reference to the VC ). Then You can ask the view controller for rotation changes.
Its a bit messy, but if You have some kind of rotation tracking in Your view controller You can filter the setNeedsDisplay with a simple if statement.
I had similar challenged updating cells that were already displayed and off the screen. While cycling through ALLL cells may not be possible - refreshing / looping through non-visible ones is.
IF this is your use case - then read on. Pre - Warning - if you're adding this sort of code - explain why you're doing it. It's kind of anti pattern - but can help fix that bug and help ship your app albeit adding needless complexity. Don't use this in multiple spots in app.
Any collectionviewcell that's de-initialized (off the screen and being recylced) should be unsubscribed automatically.
Notification Pattern
let kUpdateButtonBarCell = NSNotification.Name("kUpdateButtonBarCell")
class Notificator {
static func fireNotification(notificationName: NSNotification.Name) {
NotificationCenter.default.post(name: notificationName, object: nil)
}
}
extension UICollectionViewCell{
func listenForBackgroundChanges(){
NotificationCenter.default.removeObserver(self, name: kUpdateButtonBarCell, object: nil)
NotificationCenter.default.addObserver(forName:kUpdateButtonBarCell, object: nil, queue: OperationQueue.main, using: { (note) in
print( " contentView: ",self.contentView)
})
}
}
override func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell! {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("die", forIndexPath: indexPath) as UICollectionViewCell
cell.listenForBackgroundChanges()
return cell
}
// Where appropriate broadcast notification to hook into all cells past and present
Notificator.fireNotification(notificationName: kUpdateButtonBarCell)
Delegate Pattern
It's possible to simplify this.... an exercise for the reader. just do not retain the cells (use a weak link) - otherwise you'll have memory leaks.
if UIImageView were my class I would have solved this by doing:
class myUIImageView{
var image : UIImage? {
didSet {
if self.image != nil{
//do my stuff
}
}
}
//Other fields and functions
}
What I want is to be notified (Observer Like)
I did find the answer... Swift allows to override var so there is no real complication here. Just change where you use UIImageView to be using myUIImageView.
class myUIImageView : UIImageView{
override var image: UIImage?{
didSet {
if image != nil{
//do your stuff (add effects, layers, ...)
} else {
//clean your filter or added layer (remove your effects over the view)
}
}
}
}
UPDATE
Be advice, if your UIImageView is part of a Collection View you should remove the effect when the collection reuses your view. Otherwise it will remain visible even if no image is being shown.
You should treat a view controller's views as private. The only object that should be changing a UIImageView is the UIViewController it belongs to.
Assuming you do that, then the view controller can do whatever you want when it changes the image view's image.
In swift, I have a UI Table View and I have the background set as clear for a transparent background, and for the iPhone it works perfectly. But for iPad, it does not, it has a white background, not clear. I saw an answer, but it wasn't for swift, but that didn't work either.
My code for the iPhone is:
tableview.backgroundcolor = UIColor.clearcolor()
I tried adding:
tableview.background = nil
But that doesn't work.
I ran into the same problem. It seems that somewhere in the process of adding a UITableView to the window (between willMoveToWindow and didMoveToWindow), some iPad's reset the backgroundColor of the table view to white. It does this covertly without using the backgroundColor property.
I now use this as a base class in place of UITableView when I need a colored/transparent table...
class ColorableTableView : UITableView {
var _backgroundColor:UIColor?
override var backgroundColor:UIColor? {
didSet {
_backgroundColor = backgroundColor
}
}
override func didMoveToWindow() {
backgroundColor = _backgroundColor
super.didMoveToWindow()
}
}
EDIT: Cells also have their backgroundColor's set to white on my iPad in the same way (i.e. those that are in the table during the move to the window), so the same applies to them, lest you end up with the odd opaque cell popping up from time to time as it is reused ...
class ColorableTableViewCell : UITableViewCell {
var _backgroundColor:UIColor?
override var backgroundColor:UIColor? {
didSet {
_backgroundColor = backgroundColor
}
}
override func didMoveToWindow() {
backgroundColor = _backgroundColor
super.didMoveToWindow()
}
}
Where are you inserting that code? I had a similar problem recently with a tableview that I had subclassed. Setting the background color to clear worked perfectly well in the subclass for iPhone but on iPad it was still displaying as white.
My solution was that I had to also put it in the viewWillAppear function on the specific tableViewController that contained the table.
// myTableViewController
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
myTableView.backgroundColor = UIColor .clearColor()
}