I have the following code:
public override void TraitCollectionDidChange(previousTraitCollection: UITraitCollection)
{
base.TraitCollectionDidChange(previousTraitCollection)
...
}
What does base.TraitCollectionDidChange() do?
This methods will be called when your size class change. So you can adapt your layout :)
Have you heard about size class ?
Size class are an apple abstraction to define a size device.
For example to Iphone X have, that values :
For Portrait :
- Compact width, regular height
For Landscape :
- Compact width, compact height
With TraitCollection you can identify when this classes changes, for example from Portrait to Landscape, with this information you can update your layout.
Here an tutorial:
https://www.hackingwithswift.com/example-code/uikit/how-to-detect-when-your-size-class-changes
Here an tutorial about size class:
https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/
Related
I have designed my app initially for the iPad and am now wanting to add functionality for an iPhone too. Is there a way to check what the device being used is, and then display a view accordingly?
Structured English:
IF current_device == iPhone THEN
DISPLAY iPhoneView
ELSE IF current_device == iPad THEN
DISPLAY iPadView
If possible I also want the iPad view to only be available horizontally and then the iPhone view to only be available vertically if possible.
What you are looking for are Size Classes.
To read current horizontal size class in SwiftUI view you can refer to the environment value of horizontalSizeClass
#Environment(\.horizontalSizeClass) var horizontalSizeClass
Then use it like this in your SwiftUI View:
var body: some View {
if horizontalSizeClass == .compact {
return CompactView() // view laid out for smaller screens, like an iPhone
} else {
return RegularView() // view laid out for wide screens, like an iPad
}
}
It is worth noting that not all iPhones are compact horizontally, and compact size class is present on iPad while in multitasking configuration. You will find all possible combinations here under the Device Size Classes and Multitasking Size Classes sections.
Some articles that may be helpful
How to create different layouts using size classes
Changing a view’s layout in response to size classes
Alternatively you could set an individual threshold based on the devices height (or width) using a variable like this:
#State var isLargeDevice: Bool = {
if UIScreen.main.bounds.height > 800 {
return true
} else {
return false
}
}()
Fire up Xcode and for clarity build only to say 9.3, universal app. So, compare 9.3 iPads with 9.3 iPhones. Build to both simulator and devices - issue exhibits on both.
The app rotates in all four directions.
Have a typical situation where you do something like this...
#IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }
let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction
self.view.layoutIfNeeded() // holy! read on....
}
}
Notice the layoutIfNeeded() after the change to the constraint.
Continuing the typical example, you will have something like
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
heightFraction = (heightFraction)
// use "autolayout power" for perfection every pass.
// now that basic height/position is set,
save/load reactive positions...
position detail stuff...
}
Check it out ... I was doing this all day and only happened to use iPhones.
Interestingly you do not need the layoutIfNeeded call:
#IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }
let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction
}
}
Works fine.
However at the end of the day I put it on some iPads and .... everything broke!
Whenever you rotate landscape/portrait, problems.
After a head scratch, I realized that incredibly you do need the layoutIfNeeded call, on iPad. That's on the identical OS.
Indeed the behavior exhibits regardless of OS version. And it exhibits for ALL iPhones / ALL iPads.
#IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
{
didSet
{
if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
if ( heightFraction < minHeight ) { heightFraction = minHeight }
let h = view.bounds.size.height
spaceshipHeightPerScreen.constant = h * heightFraction
self.view.layoutIfNeeded() //MUST HAVE, IN IPAD CASE!!!!!!
}
}
To me it is incredibly troubling that they would work differently.
What I'm wondering is, is there perhaps a setting somewhere to make them work the same? Could it be my fault somehow?
Are there any other know differences between the two - or indeed is it "known" that there are a few bugs like this?
I can't think of anything odd or unusual I did anywhere, except the whole app has override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { return .All } in the first view as is normal if you want to turn the device upside down; I doubt it's related. Other than that it's a very "clean" fresh app.
It gave me a glitch-in-the-matrix feeling - it was terrifying.
What could cause this?
Per RobM's question, the SimulatedMetrics settings (Attributes tab) on the initial ViewController are...
General scheme of the app: the first scene "General" is full-screen, the size of the device. There's a container to "Live" which is the same size (using "Trailing" etc/ constraints as zero all round). In Live, there's a container view "Quad" which indeed is also fully sized to "Live," so it's also fullscreen. Quad:UIViewController exhibits the issue I describe. Quad contains various objects (images, custom controls etc) which sit around on the view. When the app launches, all is fine.
On rotation of the device (or similar): just after the change to the constraint (I don't know if that's relevant): the layoutIfNeeded call IS needed for iPads (all iPads), but is NOT needed for iPhones (all iPhones). The behavior is identical in the simulator and on devices.
Another example
I found another astounding example of this.
In a UICollectionView, custom cells (just simple static sized cells). If you happen to change a constraint (imagine say resizing an icon or product shot within the cell).
On iPad you do have to be sure to readjust in layoutIfNeeded or it will not work on the first appearance of the cell.
Whereas on iPhone it definitely behaves differently: it will "do that for you", before the first appearance of the cell, if you happen to omit it.
I tested that on every iPad and every iPhone. (Also, the unusual behavior exhibits exactly on devices or simulators: simulator makes no difference.)
I'm not able to reproduce what you're seeing; it would be nice to see a complete example. In my mockup I configured a view controller with a view having a single subview, with a constraint to control the subview height. I altered the subview height constraint in viewDidLayout based on the view size. The behavior was identical for both iPhone and iPad, and worked sans calling layoutIfNeeded on any view.
That said, I think you're changing subview constraints once the view has completed its layout - yes? I think the better way to do that would be to layout your subviews ahead of that, via viewWillTransitionToSize:withTransitionCoordinator:.
func viewWillTransitionToSize(_ size: CGSize,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
This way auto layout for the view hierarchy can complete in a single pass. This method is only called when the view is changing size, so it won't be called when the view is first loaded; you'll have to set up your initial constraints somewhere else - since they're dependent on view size perhaps you can use viewWillAppear.
Alternatively, (and possibly more correctly), subclass your view controller's view and override updateConstraints. This is the most appropriate place for changing your constraint constants.
Finally, in your property setter, you shouldn't ever call view.layoutIfNeeded(). If anything, you can set view.setNeedsLayout() so that layout happens in the next runloop iteration, and picks up ALL changes that may need to be represented.
The default simulated metrics size is “inferred”, which (if the scene isn't the target of a segue or relationship) gives you a 600x600 view, which doesn't correspond to the screen size of any iOS device. You changed the simulated metrics size at some point to “iPhone 5.5-inch”, probably to match the size of your main test device.
When a view is loaded from a storyboard (or xib), it's loaded at the size it had in the storyboard. It may then be resized by its container (either the UIWindow if it's the root view of the app, or by its superview if it's the root view of a contained view controller).
In your case, it sounds like your main test device's screen has the same size as the root view in your storyboard, so the test device doesn't have to run as much layout as you might expect.
When you use a test device whose screen size differs from your root view's size in the storyboard, the test device has to do more layout.
I didn't try to reproduce your problem, so I'm not claiming that this is a full explanation of what you're seeing. There may well be an iOS bug involved. Nevertheless, this should explain why your app behaves differently on different devices. I believe this is also why Apple chose the default inferred size of 600x600: since no device screen is that size, all devices will have to do the same amount of layout.
Here is my problem: rotate an iPhone 6 Plus or an iPad to e.g. LandscapeLeft.
Lets say your app has the following code (only for example)
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
// mine does use All, but with this case you'll understand my question
}
How do I detect if the device should use vertical or horizontal layout when
func willTransitionToTraitCollection(newCollection: UITraitCollection, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
is called?
print(UIScreen.mainScreen().bounds.width < UIScreen.mainScreen().bounds.height)
// will return false, because we started in landscape but the layout will be forced to be vertical here
print(UIApplication.sharedApplication().statusBarOrientation == .LandscapeLeft)
// will return true, because we started in landscape but the layout will be forced to be vertical here
The main problem appears on the iPad, because both size classes are regular.
I'm confused and have no idea how to solve this issue.
Don't think of your layouts as vertical and horizontal. Just let the app rotate, and respond to the size class or the size change.
On the iPad, try to avoid having two layouts. Just use autolayout to rejigger the proportions automatically. But if you must respond to rotation on the iPad, then respond to viewWillTransitionToSize (because the trait collection won't change).
I'm using a UICollectionView with horizontal scrolling (Layout = UICollectionViewFlowLayout). After rotation I adapt the content offset so that the same items are displayed before the rotation. On rotation the width of the items are changed, because there are always seven items on screen. My current approach does work for iOS 8, but not for iOS 7.
On iOS 7 layoutAttributesForItemAtIndexPath always returns the same X position regardless of the orientation. So it seems that some frame is not correctly adapted. The calculation of the size of the items seems to be correct, because they changes accordingly.
This is my code:
My main view controller:
public override void DidRotate (UIInterfaceOrientation fromInterfaceOrientation)
{
base.DidRotate (fromInterfaceOrientation);
myProblematicCollectionView.invalidateLayout ();
}
The method from the collection view:
public void invalidateLayout(){
Layout.InvalidateLayout ();
UICollectionViewLayoutAttributes layoutAttributesForItem = CollectionView.GetLayoutAttributesForItem (NSIndexPath.FromItemSection (0, CurrentVisibleSection));
CollectionView.SetContentOffset (new CGPoint (layoutAttributesForItem.Frame.X, 0), false);
}
The code is in C# but it shouldn't bother you. You can of corse post solutions in Objective-C or Swift.
CurrentVisibleSection is the section with the seven items. It is always up-to-date. When GetLayoutAttributesForItem or layoutAttributesForItemAtIndexPath is called, the GetSizeForItem or collectionView:layout:sizeForItemAtIndexPath: is called before the content offset is set.
Has anyone a clue why I always get the same layout attribute regardless of the orientation?
Edit:
If I use reloadData before calling layoutAttributesForItemAtIndexPath I get the correct size. Do I need this after invalidateLayout?
What I also don't get is that the collection view has the correct size (frame width is correct).
i'm trying to create a simple collectionView like pinterest. I've reached a problem i've set the margins like in the image, but as u can see the middle margin is 20 since both right and left is 10 how can i make so that its also 10. i've tried changing some values, but it is not working.
for fitting all orientations
Is there a better way?
func collectionView(collectionView : UICollectionView,layout collectionViewLayout:UICollectionViewLayout,sizeForItemAtIndexPath indexPath:NSIndexPath) -> CGSize
{
return CGSizeMake(self.collectionView!.frame.width/2-20, self.collectionView!.frame.width/2-20+50)
}
Use auto layout and view constraints. This will allow you to have each tile maintain its spacing and scale for different orientations.
see the apple docs