在iPhone 5.1模拟器和运行iOS 5.1的真实iPhone 4上,应用程序表现不同。

29

简而言之:

当导航控制器的返回按钮标题更改时,在某些情况下,旧标题会卡住,新标题将不会显示。这只发生在一些可重现的情况下,而在其他情况下,它按设计工作。

这取决于硬件

  • 错误发生在iPhone 3G(iOS 4.2.1)和模拟器(iOS 5.1)上
  • 使用相同的源代码,在iPhone 4(iOS 5.1)上没有错误

这取决于写入标题的单词

  • 当按钮被创建时,从我自己编写的创建方法中获取与上一个在导航控制器堆栈上的页面相同的标题,如果其他条件匹配,则尝试在以后更改按钮的标题时,旧文本会被固定,新标题将不会显示出来。
  • 当按钮在创建时获取与其默认标题不同的单词作为标题时,以后的每次更改都可以正常工作,只要不将默认标题分配给它即可。
  • 如果在成功更改了许多不同标题的情况下,将字词放在按钮的标题上,这是其默认标题,则该字词会被固定。稍后的更改将不会被接受(没有任何消息,只有在其他条件匹配的情况下)

这取决于按钮在此期间是否隐形。

  • 如果导航控制器的栈上推入了另一个视图,以使带有错误按钮的旧页面被新页面隐藏,并且稍后从堆栈中弹出新页面,使按钮再次可见(并且其他情况匹配),则旧文本将被卡住,试图更改它将被忽略(没有任何消息)。
  • 如果按钮从未被隐藏,则更改其标题永远不会有问题。它总是有效的。

正确的标题在动画期间可见

  • 当尝试更改返回按钮的标题由于上述情况的组合而被忽略时,当单击此返回按钮并处理页面向右滑动动画时,适当的标题仍然会在约0.3秒钟内变得可见。在动画开始时,旧的卡住的标题被正确的标题替换,正确的标题在动画期间可见。

详细描述

这是关于 UINavigationController 返回按钮上的文本问题。我根据新的语言设置更改此按钮的标题。目前我的应用程序在导航控制器堆栈中最多有3个视图控制器。它们中的每一个都是 `UITableViewController` 的不同子类。

表1,名为 GeneralTableVC 是堆栈上的根视图。它没有返回按钮。它向用户提供了存储在应用程序内的内容摘要,并显示带有设置按钮的工具栏。

提供在表1中可见的工具栏的是导航控制器。在表2和3中将其设置为不可见。目前该工具栏中只有一个名为“设置”的按钮。触摸此设置按钮将把表2推送到堆栈上。

表2,名为 SettingsTabVC 有一个返回按钮,在模拟器中会出现问题,但在运行iOS 5.1的真实iPhone 4上正常工作。

通过触摸表2的第一行,将创建一个新的表(表3)并将其推送到堆栈上。

表格3名为LangSelectTableVC,也有一个返回按钮,但这个按钮在iPhone模拟器和真实的iPhone 4设备中都能很好地工作。

表格3是一个语言选择表格,显示所有可用语言(目前只有英语和德语)的列表。触摸行会立即更改设置。活动视图(表格3)将被重新绘制,在几毫秒内屏幕上的所有文本都将以新语言显示。

重新绘制表格本身并不是问题,导航栏中的标题也是如此。但是返回按钮上的文本也必须进行翻译,这有点棘手。我已经在两个返回按钮上使用了完全相同的技巧,对于指向表格2的按钮在表格3上可见,它可以很好地工作。但是,使用完全相同的代码,模拟器上的指向表格1的按钮存在问题(但真实的iPhone上没有问题)。

我给你一些代码片段和一些截图,以展示我所做的和发生的情况:


源代码

使用ARC(自动引用计数)。

我定义了一个重绘协议:

Protocols.h

#ifndef ToDo_Project_Protocols_h
#define ToDo_Project_Protocols_h

@protocol redrawProt
- (void) mustRedraw;
@end

#endif

这是表格1的标题:

GeneralTableVC.h

#import <UIKit/UIKit.h>
#import "Protocols.h"
// some other imports

@interface GeneralTabVC : UITableViewController <redrawProt>

@property id<redrawProt>   parent;
@property Boolean          mustRedrawMyself;
@property NSString*        backTitle;
@property UIBarButtonItem* myBackButton;
@property UIBarButtonItem* parBackButton;

- (id) initWithParent:(id<redrawProt>)par andBackTitle:(NSString*)bT andBackButton:(UIBarButtonItem*)bB;

@end

其他表的头文件,SettingsTabVC.hLangSelectTabVC.h 定义了相同的属性和相同的初始化函数。
程序从这里开始: AppDelegate.m 的一部分
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // some code
    GeneralTabVC* genTabCon = [[GeneralTabVC alloc] initWithParent:nil andBackTitle:nil andBackButton:nil];
    UINavigationController* navCon = [[UINavigationController alloc] initWithRootViewController:genTabCon];
    // some other code
}

接下来是Table1的实现(GeneralTableVC.m)。表2(SettingsTabVC.m)和表3(LangSelectTabVC.m)中的代码类似。我不显示实现协议UITableViewDataSource的代码部分,因为我认为这些部分并不重要来解释问题。
在这段代码中,您将找到宏LocalizedString(keyword),它与NSLocalizedString(keyword,comment)完全相同,可以将关键字翻译成所需的语言。我的版本使用不同的包进行翻译(而不是主包)。

GeneralTableVC.m

#import "GeneralTabVC.h"
#import "SettingsTabVC.h"

#define MYTITLE @"summary"

id<redrawProt>   parent;
Boolean          mustRedrawMyself;
NSString*        backTitle;
UIBarButtonItem* myBackButton;
UIBarButtonItem* parBackButton;

@interface GeneralTabVC ()

@end

@implementation GeneralTabVC

@synthesize parent, mustRedrawMyself, backTitle, myBackButton, parBackButton;

- (void) mustRedraw {
    self.mustRedrawMyself = YES;
}

- (void) redraw {
    if ((self.parBackButton) && (self.backTitle)) {
        // Important!
        // here I change the back buttons title!
        self.parBackButton.title = LocalizedString(self.backTitle);
    }
    if (self.parent) {
        [self.parent mustRedraw];
    }
    self.title = LocalizedString(MYTITLE);
    [self.tableView reloadData];
    self.mustRedrawMyself = NO;
}

- (id) initWithParent:(id<redrawProt>)par andBackTitle:(NSString*)bT andBackButton:(UIBarButtonItem *)bB {
    self = [super initWithStyle:UITableViewStyleGrouped];
    if (self) {
        self.parent = par;
        self.mustRedrawMyself = NO;
        self.backTitle = bT;
        self.parBackButton = bB;
    }
    return self;
}

- (void) toolbarInit {
    // this method exists only in Table 1, not in other tables
    // it creates a UIBarButtonItem, adds it to self.toolbarItems
    // and makes it visible
}

- (void)SettingsAction:(id)sender {
    // this method exists only in Table 1, not in other tables
    // it will be executed after the user tabs on the settings-
    // button in the toolbar
    SettingsTabVC* setTabCon = [[SettingsTabVC alloc] initWithParent:self andBackTitle:MYTITLE andBackButton:self.myBackButton];
    [self.navigationController pushViewController:setTabCon animated:YES];
}

- (void) viewDidLoad {
    [super viewDidLoad];
    self.title = LocalizedString(MYTITLE);    
    // I want an Edit-Button. Localization of this button is
    // not yet done. At the moment is uses the systems language,
    // not the apps language.
    self.navigationItem.rightBarButtonItem = self.editButtonItem;
    [self toolbarInit];    
}

- (void) viewWillAppear:(BOOL)animated {    
    // this is an important method! Maybe here is the reason for 
    // my problem! 
    [super viewWillAppear:animated];
    // When ever this controllers view is going to appear, and  
    // when ever it is necessary to redraw it in a new language,
    // it will redraw itself:
    if (self.mustRedrawMyself) {
        [self redraw];
    }

    // And here comes the buggy back button:
    // When ever this controllers view is going to appear,
    // a new back button will be created with a title in the
    // new language:
    UIBarButtonItem* BB = [[UIBarButtonItem alloc] init];
    BB.title = LocalizedString(MYTITLE);
    self.myBackButton = BB;
    self.navigationItem.backBarButtonItem = self.myBackButton;
}

- (void) viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    // show toolbar:
    [self.navigationController setToolbarHidden:NO animated:YES];
}

// next methods are about InterfaceOrientation and the
// UITableViewDataSource protocoll. They are not important
// for the problem.

// but maybe the very last method is important. It comes in
// different versions in the three implementation files:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // This is the version of GeneralTableVC.m (Table 1)
    // It does nothing (at the actual stage of expansion, in later
    // versions it will start the main business logic of this app)
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // This is the version of SettingsTableVC.m (Table 2)
    // Tabbing onto row 0 of section 0 will push the
    // language-selection-table (Table 3) on screen:
    if (indexPath.section == 0) {
        if (indexPath.row == 0) {
            // create Table 3:
            LangSelectTabVC* langTabCon = [[LangSelectTabVC alloc] initWithParent:self andBackTitle:MYTITLE andBackButton:self.myBackButton];
            [self.navigationController pushViewController:langTabCon animated:YES];
        } else {
            // do something else (nothing at this stage of expansion)
        }
    } else {
        // do something else (nothing at this stage of expansion)
    }
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // This is the version of LangSelectTableVC.m (Table 3)

    // here I do some magic to select and store the new language.
    // Part of this magic is transforming indexPath.row
    // into a valid language-code, putting it into the 
    // settings-object, and registering this object to 
    // NSUserDefaults
}

@end

截图

在iPhone 5.1模拟器上启动应用程序将在屏幕上显示表格1 (GeneralTableVC):

after starting the app

在屏幕按钮工具栏上,其右侧有一个设置按钮。按下此按钮会将下一个表格显示在屏幕上:

settings-screen

请注意标题栏中的后退按钮。它显示的文本是“Summary”,这是正确的,因为前一个表格标题是“Summary”。
现在我们切换到第一行(“Language English >”):

before changing language

一切都很好。 现在让我们改变语言。点击 "German" 标签:

after changing language

哇!现在一切都是德语了。甚至后退按钮也从“设置”变成了“Einstellungen”。

让我们点击那个“Einstellungen”后退按钮:

back button has wrong language in simulator

几乎一切都很好了;一切都变成了德语。除了返回按钮,它仍然显示“Summary”而不是“Überblick”。我不明白为什么,因为当我在我的真正的iPhone 4上使用完全相同的步骤和源代码时,最后一个屏幕看起来像这样:

correct langugage on real iPhone

请注意后退按钮上的文字。在真正的 iPhone 4 上,它是德语单词“Überblick”(这就是我想要的),但在模拟器中,它是英语单词“Summary”。 这对我来说意味着,在某些手机上(像我的 iPhone 4),用户得到了预期的结果,但是在一些其他手机上(也许是 iPhone 4S),用户会得到一个有错误的显示。
有人有什么关于我的代码出了什么问题的想法吗?

编辑

编辑:2012年4月6日09:04 +02:00(中欧夏令时)

我设法在另一台硬件上测试我的应用程序,一台旧的iPhone 3G(iOS 4.2.1)。在旧的iPhone上,我的应用程序的行为与模拟器完全相同。在iPhone 4上运行相同的应用程序会产生不同的行为。

更准确地说:

  • 在iPhone 4(iOS 5.1)上:应用程序正在执行我想要的操作,没有错误的行为。
  • 在模拟器(iOS 5.1)上:应用程序在导航控制器的返回按钮上显示错误的标题。
  • 在iPhone 3G(iOS 4.2.1)上:应用程序显示与模拟器中相同的错误行为。

编辑:2012年4月7日10:14 +02:00(中欧夏令时)

通过观察iPhone 3G上的过渡效果,我发现了一些有趣的、可能有用的东西:当我点击带有错误文本的按钮时,会发生以下情况:

  1. 错误文本被正确文本替换
  2. 在此替换后,视图以动画方式消失(向右滑动),下面的视图变得可见。这个转换持续大约0.3秒,在这个短时间间隔内,所有硬件-iPhone和模拟器中都可以看到正确的文本。

但问题仍然是:为什么iPhone 3G和模拟器显示错误的文本?为什么在iPhone 4中始终显示正确的文本?

对我来说,看起来好像在同一个位置有两个按钮,一个在另一个上面。在iPhone 4中,“我的”自定义按钮在前面,隐藏了旧的系统生成按钮,但在模拟器和iPhone 3G中,旧的系统生成按钮在前面,隐藏了我的自定义按钮。但是:即使我的隐藏自定义按钮比系统生成的按钮更大(更宽),也没有任何可见部分。只有在滑出动画开始时,我的按钮才变得可见。


编辑:2012年4月7日16:38 +02:00(中欧夏令时)

下一个有趣的事实:

到目前为止发生的情况如下:

当按钮第一次出现时(第二张截图,请参见下文),我将一个单词作为标题放在按钮上,该单词与系统之前生成的单词相同。然后用户选择某个操作,该按钮被另一个视图隐藏。在另一个用户操作之后,该按钮再次显示,现在它应该获得一个新的单词作为标题(意思相同,但是是新的语言),但是在iPhone 3G和模拟器上,旧标题更“强壮”。新标题不会显示。旧标题仍然存在。

如果我首先将一个与系统生成的标题不同的单词写成标题放在按钮上,则不会发生这种情况。如果第一个标题与默认标题不同,则稍后的更改将在所有iPhone和模拟器上执行。

这让我相信,iOS 进行了某种“优化”:如果在按钮首次出现时,自定义标题与系统生成的标题完全相同,则稍后更改按钮标题将被忽略,但仅适用于 iPhone 3G 和模拟器。在 iPhone 4 上,无论如何都可以允许稍后进行更改。但是,在一开始设置不同的标题以防止应用程序出现故障行为并不是一个选项。

这几乎无法解释问题发生的原因,但您能否回应一下需要通过删除旧的parBackButton、构建一个带有正确标题的新按钮并将其添加到视图的情况?我想知道这是否会有所不同。 - Cowirrie
抱歉,Dondragmer,我的英语太弱了,无法完全理解你在两行半的问题中询问我的内容。请你能否将其分成更多但较小的句子和问题?请使用更简单的语法结构。谢谢。 - Hubert Schölnast
如果需要更改parBackButton.title,则销毁它并构建一个新的。 - Cowirrie
既然你有点怀疑“你的”返回按钮和“系统”的返回按钮之间存在冲突,那么尝试一下以下操作的结果是什么:[yourBackButton.superview bringSubviewToFront: yourBackButton]? - verec
1
非常好的问题。+1 - Bo A
显示剩余2条评论
3个回答

2
我怀疑你遇到的问题是模拟器和真实硬件之间微妙的时间差异导致的。
视图控制器不一定在viewDidLoad中实例化,因此您应该等到viewWillAppear时设置标题值(等等)。
以下内容是建设性的,请以既定的精神接受:
没有详细审查您的代码,我怀疑您尝试实现的目标可以更可靠地实现。您要做的并不难或不寻常,但恐怕您的代码看起来过于复杂——可能是由于您试图解决这些时间问题。
查看一些简单的示例和教程,并尝试简化您的代码,使其不使用标志跟踪状态(mustRedrawMyself),因为这不应该是必要的。记住,在viewWillAppear之前不要设置视图/控件的属性,然后看看您的情况如何。
如果您还没有使用内置的本地化支持,您也可以考虑一下。
祝你好运。

我确实在viewWillAppear中创建并设置按钮!每个视图都必须重新绘制自己,因此告诉一个不可见的视图在变得可见时重新绘制自己的简单方法是在该视图上设置一个标志。这就是为什么需要mustRedrawMyself。我还详细研究了本地化的内置支持,但是这种内置支持要求在用户更改语言时关闭并重新启动应用程序。这不是我想要的。但无论如何,本地化都不是问题。问题在于更改按钮的标题! - Hubert Schölnast
1
我可以补充一下,我遇到过几次时间问题。@HubertSchölnast,请尝试在工作的iPhone上逐步执行代码,看看会发生什么-我的猜测是它的行为会改变... - Magnus

0
我会尝试这样做:
将SELF REDRAW的执行放到你在viewWillAppear中更新本地化之后:
你正在告诉视图在更改标题之前重新绘制,因此标题更新不是重新绘制的一部分。它正在完全按照它应该做的...我猜测某些硬件/模拟器的时间操作更快,因此您在重新绘制调用后进行的更新在某些硬件/模拟器上发生在绘制完成之前,但在其他硬件/模拟器上没有完成。
尝试按照以下操作顺序更改,然后让我知道是否有效。
- (void) viewWillAppear:(BOOL)animated {
     // this is an important method! Maybe here is the reason for 
 // my problem!  
[super viewWillAppear:animated];
// And here comes the buggy back button: 
// When ever this controllers view is going to appear, 
// a new back button will be created with a title in the  
// new language:  
UIBarButtonItem* BB = [[UIBarButtonItem alloc] init]; 
BB.title = LocalizedString(MYTITLE);
 self.myBackButton = BB; 
self.navigationItem.backBarButtonItem = self.myBackButton; 

 // When ever this controllers view is going to appear, and 
  // when ever it is necessary to redraw it in a new language,  
// it will redraw itself: 
if (self.mustRedrawMyself) { 
    [self redraw];   
}  

} 

我尝试了你的建议,但什么都没有改变。你有没有在开头阅读整个“简而言之”部分?一个时间问题如何解释错误取决于标题中的单词(“Summary”或“xyz”)?在方法“viewWillAppear”中出现的时间问题如何解释,在页面消失动画期间几分钟后才显示正确的标题? - Hubert Schölnast
在您的原始代码中,您告诉视图在更改标题为本地化版本之前重新绘制。作为下一步,我会在viewWillAppear中删除重绘周围的IF(),并在更改BB.TItle后告诉它重新绘制,以确保mustRedrawMyself值在您期望它为yes时不是NO。至于为什么这很重要,请看您的原始逻辑。您告诉视图在更改值为本地化版本之前重新绘制。当溶解发生时,您的更改已经完成,因此这些重绘将采用新值。 - Speckpgh
关于我为什么建议检查时间问题,我并不确定绘图是否发生在主UI线程上。如果不是,并且在单独的线程上,那么仅仅因为你的代码中步骤是顺序执行的,并不意味着它们会按顺序执行。我并不是说我确切知道答案,我只是说这是我会尝试的方法。我会确保在调用重绘之前要绘制的值已经改变,并验证重绘调用确实发生了,而且IF语句没有跳过它,因为值是错误的。 - Speckpgh
我不确定你想要详细告诉我什么,因为我的英语太弱了。但我确实明白你想让我在viewWillAppear中删除[self redraw];周围的if。我尝试了一下,但没有任何变化(除了视图每次出现时都会重新绘制,这通常是不必要的)。标志mustRedrawMyself-(void)mustRedraw{...}中设置。此方法在viewWillAppear被调用之前几秒甚至几分钟就已经被调用了。因此,mustRedrawMyself不可能存在竞争条件。 - Hubert Schölnast

0

苹果支持已回复

我联系了苹果支持解决这个问题,他们已经回复了。

问题是,导航栏保存了导航控制器堆栈上所有视图的返回按钮,并且所有这些按钮需要同时更新。在它们的viewWillAppear方法中更新位于堆栈上的视图是好的,但尝试在此位置更新返回按钮是不好的想法。


解决方案:

扩展UIViewController的接口:

@interface UIViewController (extended)
    - (NSString *)localizedKey;
@end

对于将视图放入 UINavigationController 堆栈的每个 UIViewController,请实现此方法:
- (NSString*) localizedKey {
    return @"a title-keyword";
}

在任何UIViewControllers中不要随意更改UIBarButtonItemself.navigationItem.backBarButtonItem

当需要更改标题时,请使用以下代码片段同时更改所有返回按钮的标题(请记住:LocalizedString(key)是类似于NSLocalizedString(key,comment)的自定义宏):

NSArray* vcs = [self.navigationController viewControllers];
for (UIViewController* vc in vcs) {
    vc.navigationItem.backBarButtonItem.title = LocalizedString([vc localizedKey]);
    vc.title = LocalizedString([vc localizedKey]);
}

苹果支持的逐字回答:

我们正在解决导航栏在错误的时间强制更新的问题。请注意,每个视图控制器中的所有视图都会得到正确的更新。因此,导航栏需要特别关注才能得到我们想要的结果。
为了使其正常工作,您需要一次性更改堆栈上所有视图控制器的返回按钮(在用户选择语言时),而不是在它们每次出现时通过“viewWillAppear”更改。
这需要以公共方式获取该按钮的本地化键的能力。我引入了一个类别来扩展UIViewController以轻松实现此目的:
@interface UIViewController (extended) - (NSString *)localizedKey; @end
然后,您的LangSelectTabVC类可以一次性更改所有返回按钮。这种方法将使按钮标题正确重绘。
因此,在viewWillAppear中,您不必更新每个返回按钮。在那里做这件事似乎太晚了,UIKit无法捕获该更新。当更新发生时,您还会重新创建一个新的返回按钮。这是不必要的,只需取当前的按钮并更改其标题即可:
NSArray *vcs = [self.navigationController viewControllers]; for (UIViewController *vc in vcs) { vc.navigationItem.backBarButtonItem.title = LocalizedString([vc localizedKey]); vc.title = LocalizedString([vc localizedKey]); }
我附加了一个修改后的项目,展示了这个解决方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接