【问题标题】:Alternative iOS layouts for portrait and landscape using just one .xib file仅使用一个 .xib 文件的纵向和横向替代 iOS 布局
【发布时间】:2013-09-06 21:09:29
【问题描述】:

使用 xcode 中的界面生成器和一个 .xib 文件,我如何在横向和纵向之间旋转时创建替代布局?

查看不同布局的图表

注意。绿色视图/区域将包含 3 个横向横向流动的项目,而纵向这 3 个项目将在绿色视图/区域内垂直流动。

【问题讨论】:

  • 我的回答还不足以解决您的问题吗?
  • 谢谢,但从技术上讲,您没有回答我的问题,我问如何用一个笔尖完成。此外,赏金还剩 5 天。我会等着看其他人是否有解决方案。抱紧
  • 随着 iOS 9 中 UIStackView 的引入,这种布局非常容易。将视图放入堆栈视图,加载视图后,您只需根据屏幕方向将堆栈视图的方向翻转为垂直或水平。系统将为您处理所有布局更改。
  • @Jake 不错。在 iOS9 之前的设备上使用 UIStackView 会发生什么?
  • @DaveHaigh 我假设在 iOS9 之前的设备上使用 UIStackView 会使应用程序崩溃,除非你 check for availability of the class 并为 iOS9 之前的设备提供替代实现

标签: ios xcode interface-builder xib screen-orientation


【解决方案1】:

一种方法是在您的 .xib 文件中包含三个视图。第一个是没有子视图的 Viewcontroller 的普通视图。

然后根据需要创建纵向和横向视图。这三个都必须是根级视图(看截图)

在您的 Viewcontroller 中,创建 2 个 IBOutlets,一个用于纵向视图,一个用于横向视图,并将它们与界面构建器中的相应视图连接:

IBOutlet UIView *_portraitView;
IBOutlet UIView *_landscapeView;
UIView *_currentView;

需要第三个视图_currentView 来跟踪当前正在显示这些视图中的哪一个。 然后像这样创建一个新函数:

-(void)setUpViewForOrientation:(UIInterfaceOrientation)orientation
{
    [_currentView removeFromSuperview];
    if(UIInterfaceOrientationIsLandscape(orientation))
    {
        [self.view addSubview:_landscapeView];
        _currentView = _landscapeView;
    }
    else
    {
        [self.view addSubview:_portraitView];
        _currentView = _portraitView;
    }
}

你需要从两个不同的地方调用这个函数,首先是初始化:

-(void)viewDidLoad
{
    [super viewDidLoad];
    UIInterfaceOrientation interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
    [self setUpViewForOrientation:interfaceOrientation];
}

第二个是方向变化:

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [self setUpViewForOrientation:toInterfaceOrientation];
}

希望对你有帮助!

【讨论】:

  • 感谢 MeXx。结合@TomSwift的回答,这似乎是实现我所要求的方法。
  • 加载第二个视图可能不会为转换设置动画。因此,它不会向用户提供有关哪个组件移动到哪个位置的反馈。
  • 一个大问题是您如何处理 IBOutlets?如果有,请在两个版本上使用相同的标签。您只能将一个 IBOutlet 连接到您的控制器....
  • 我想从 2 个不同的视图处理插座的唯一方法是在当前使用的插座上有一个指针。然后在切换布局时更新指针,就像他对 _currentView 所做的那样
  • 我在处理横向/纵向后遇到了同样的问题。有没有关于它的链接?谢谢。
【解决方案2】:

没有自动的方法来支持它,因为它违反了 Apple 的设计。您应该有一个支持两种方向的 ViewController。

但如果你真的想这样做,你需要在旋转事件上重新加载 xib 并加载不同的 nib 文件。

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    [[NSBundle mainBundle] loadNibNamed:[self nibNameForInterfaceOrientation:toInterfaceOrientation] owner:self options:nil];
    [self viewDidLoad];
}

- (NSString*) nibNameForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    NSString *postfix = (UIInterfaceOrientationIsLandscape(interfaceOrientation)) ? @"portrait" : @"landscape";
    return [NSString stringWithFormat:@"%@-%@", NSStringFromClass([self class]), postfix];
}

然后你创建了两个 nib 文件,后缀为“landscape”和“portrait”。

【讨论】:

  • 这不是我最初问题的答案。但这可能是我最终在我的项目中支持横向和纵向的方式,所以感谢您的回答。但我不能授予它赏金对不起
  • 这可能不是您想要的答案,但 Apple 建议您不要按照您的建议进行操作:developer.apple.com/library/ios/featuredarticles/…
  • fyi 和其他人正在阅读这篇文章,这就是我实现不同布局的方式。我完全按照您的建议根据方向加载了不同的 nib 文件。迟来的感谢:)
  • @Grzegorz 我正在使用您提供的方法,问题是如何在方向更改时保留 UITextField 的文本?
  • 如果您使用动态布局和约束,它应该会自动执行。
【解决方案3】:

我喜欢@MeXx 的解决方案,但它需要在内存中保留两个不同的视图层次结构的开销。此外,如果任何子视图的状态(例如颜色)发生变化,则需要在切换层次结构时对其进行映射。

另一种解决方案可能是使用自动布局并为每个方向交换每个子视图的约束。如果您在两个方向的子视图之间进行 1:1 映射,这将最有效。

可以理解,您希望使用 IB 直观地定义每个方向的布局。根据我的计划,您必须按照@MeXx 的规定进行操作,然后创建一种机制来在加载笔尖后存储两组约束 (awakeFromNib) 并在布局上重新应用正确的设置 (viewWillLayoutSubviews)。刮取并存储其约束后,您可以丢弃辅助视图层次结构。 (由于约束是特定于视图的,您可能会创建新的约束以应用于实际的子视图)。

抱歉,我没有任何代码。这只是现阶段的计划。

最后一点 - 使用 xib 比使用情节提要更容易,因为在情节提要中描述位于视图控制器主视图之外的视图是很痛苦的(这是可取的,否则它是要编辑的 PITA)。布莱克!

【讨论】:

  • 谢谢汤姆。结合 MeXx 的解决方案,我认为这可能有效。当他发布大部分解决方案时,我奖励了他。感谢补充
  • 你有任何示例代码来说明这一点吗?
  • 这种方法的优点是理论上它应该支持开箱即用的过渡动画,因为改变的只是布局约束,而不是视图本身。
  • @devios 我刚刚用 MeXx 的解决方案测试了一个简单的标签,令人惊讶的是,过渡动画效果很好——我认为 TomSwift 的解决方案是减少内存使用以降低速度。恕我直言,如果我们确保任何可能改变的状态(甚至是 GUI 属性)被传播到模型然后返回,那么 MeXx 的解决方案将会正常工作。请记住,在 MVC 中,我们应该能够一致地支持任意数量的视图(在本例中为纵向和横向)。
  • @devios 让我回顾一下 - 仔细观察一下,我真的应该说动画发生得如此之快,以至于很难准确判断发生了什么 - 但我认为原始小部件是旋转的然后最终的小部件就位。
【解决方案4】:

是的,先生,你当然可以......

我以前做的是在设备旋转时手动给框架

每当设备旋转时,- (void)viewWillLayoutSubviews 就会被调用

例如 - 在您的 UIView 中使用 UIButton *button

- (void)viewWillLayoutSubviews
   {
       if (UIDeviceOrientationIsLandscape([self.view interfaceOrientation]))
       {
         //x,y as you want
         [ button setFrame:CGRectMake:(x,y,button.width,button.height)];

       }
       else
       {
         //In potrait
          //x,y as you want
         [ button setFrame:CGRectMake:(x,y,button.width,button.height)];


       }

   }

这样你就可以随意放置了。谢谢

请看一下这些图片

我想要在两个屏幕上显示的 xCode XIB UIView 的第一张图片

现在我也想看风景,所以我去编辑我的-(void)viewWillLayoutSubviews

现在的结果是这样的 模拟器中人像屏幕的第一张图片

这是横向的

【讨论】:

  • 我想使用 Interface Builder 创建我的视图
  • 我不想以编程方式进行,您的示例在两个方向都有相同的布局。
  • 在自动布局控制的界面中显式设置框架最终会给您带来麻烦。我建议以编程方式而不是视图框架来调整约束。您可以为您的约束创建出口并轻松地从视图控制器修改它们的值。
【解决方案5】:

使用情节提要 [ios7] 的解决方案。在主视图控制器内部,有可以包含标签栏控制器的容器视图。在标签栏控制器内部,有 2 个标签。一个选项卡用于纵向控制器,另一个选项卡用于横向模式控制器。

主视图控制器实现了这些功能:

#define INTERFACE_ORIENTATION() ([[UIApplication sharedApplication]statusBarOrientation])

@interface MainViewController ()
@property(nonatomic,weak) UITabBarController *embeddedTabBarController;
@end

@implementation MainViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"embedContainer"])
    {
        self.embeddedTabBarController = segue.destinationViewController;
        [self.embeddedTabBarController.tabBar setHidden:YES];
    }
}

- (void) adaptToOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    if (UIInterfaceOrientationIsLandscape(interfaceOrientation))
    {
        [self.embeddedTabBarController setSelectedIndex:1];
    }
    else
    {
        [self.embeddedTabBarController setSelectedIndex:0];
    }
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration
{
    [self adaptToOrientation:interfaceOrientation];    
}

- (void) viewWillAppear:(BOOL)animated
{
    [self adaptToOrientation:INTERFACE_ORIENTATION()];
}

情节提要中的segue链接应命名为“embedContainer”。

确保容器视图可调整大小以满足父视图,无论是横向还是纵向。

请注意,在运行时,同一控制器的 2 个实例同时存在。


更新: Apple docs for alternate landscape view controller

【讨论】:

    【解决方案6】:

    我只想添加一个新答案,以反映 ios8 和 XCode 6 中即将推出的新功能。

    在最新的新软件中,Apple 引入了size classes,它使情节提要能够根据您使用的屏幕尺寸和方向智能地进行调整。虽然我建议您查看上面的文档或 WWDC 2014 会议 building adaptive apps with UIKit,但我会尝试解释一下。

    确保大小等级为enabled,

    您会注意到您的故事板文件现在是方形的。您可以在此处设置基本界面。 界面将针对所有启用的设备和方向智能调整大小。

    在屏幕底部,您会看到一个按钮,上面写着 yAny xAny。通过单击它,您可以只修改一个尺寸类别,例如横向和纵向。

    我确实建议您阅读上面的文档,但我希望这对您有所帮助,因为它只使用一个故事板。

    【讨论】:

    • 谢谢,听起来像是一个实际的解决方案(终于),我会好好读一读。
    • @DaveHaigh 我希望它有所帮助。如果您有任何问题,请告诉我。
    • 阅读后,听起来它可以完成这项工作,但还没有机会尝试一下。一旦我这样做,我会告诉你
    • 我已经花了一些时间研究尺码等级,但我不相信有一种方法可以完成所要求的事情。尺寸等级的问题是它们并不真正支持任何方向的概念,您可以通过 iPhone 的“紧凑宽度、任意高度”和横向“任意宽度紧凑高度”来获得其中的一些,但是对于在 iPad 上,您可以在两个方向上获得“常规宽度,常规高度”。
    【解决方案7】:

    您可以使用库GPOrientation。这里是link

    【讨论】:

    • 您以前在项目中使用过这个吗?
    【解决方案8】:

    以编程方式调整大小

    我有一个应用程序,我的情况完全相同。在我的 NIB 中,我只有纵向布局。另一种布局以编程方式执行。以编程方式指定几个大小并不难,因此无需维护两个 NIB。

    请注意,通过设置帧以编程方式执行视图更改将产生动画(或者可以使用动画轻松制作)。这将为用户提供有关哪个组件移动到哪个位置的有价值的反馈。 如果你只是加载第二个视图,你将失去这个优势,从 UI 的角度来看,我认为加载一个替代视图不是一个好的解决方案。

    使用两个 ViewControllers

    如果您喜欢使用两个 ViewController,那么 Apple 在其开发者文档中描述了如何做到这一点,即您可以在这里找到问题的答案:RespondingtoDeviceOrientationChanges

    如果您喜欢以编程方式执行此操作,您可以在方法 didRotateFromInterfaceOrientation 中添加重新定位代码。

    示例

    我将方法添加到我的 ViewController:

    - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
    {
        [self adjustLayoutToOrientation];
    }
    

    然后实施布局的调整。这是一个示例(代码特定于我的应用程序,但您可以读取超级视图的大小并从中计算子视图的帧数。

    - (void)adjustLayoutToOrientation
    {
        UIInterfaceOrientation toInterfaceOrientation = self.interfaceOrientation;
        bool isIPhone = !([[UIDevice currentDevice] respondsToSelector:@selector(userInterfaceIdiom)] && [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad);
    
        [self adjustViewHeight];
        [self adjustSubviewsToFrame: [mailTemplateBody.superview.superview frame]];
    
        CGRect pickerViewWrapperFrame = pickerViewWrapper.frame;
        if(isIPhone) {
            pickerViewWrapperFrame.origin.x = 0;
            pickerViewWrapperFrame.size.height = keyboardHeight;
            pickerViewWrapperFrame.origin.y = [self view].frame.size.height-pickerViewWrapperFrame.size.height;
            pickerViewWrapperFrame.size.width = [self view].frame.size.width;
        }
        else {
            pickerViewWrapperFrame = pickerView.frame;
        }
    
    // MORE ADJUSTMENTS HERE
    
    }
    

    【讨论】:

    • 这不是使用界面生成器
    • 您的问题在这一点上并不准确。我相信以编程方式使用 UI 组件是一个不错的选择,在某些情况下会更好。我会考虑重新加载替代视图是一个不好的解决方案,因为它会破坏动画。以编程方式执行此操作将为从一种视图到另一种视图的转换设置动画,这是对正在发生的事情的良好用户反馈。
    • 啊。对不起。我弄错了。我想像“基于界面构建器的布局”​​而不是“(专门)使用界面构建器”。你说得对。开发者文档developer.apple.com/library/ios/featuredarticles/… 中描述了您的用例
    猜你喜欢
    • 2019-04-03
    • 2014-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多