web-dev-qa-db-ja.com

自動レイアウトとセクションの再読み込みを備えたUITableViewHeaderFooterViewサブクラスは一緒にうまく機能しません

UITableViewHeaderFooterViewサブクラスに自動レイアウトを取り込もうとしています。クラスはかなり基本的で、2つのラベルだけです。これは完全なサブクラスです。

_@implementation MBTableDetailStyleFooterView

static void MBTableDetailStyleFooterViewCommonSetup(MBTableDetailStyleFooterView *_self) {
    UILabel *rightLabel = [[UILabel alloc] init];
    _self.rightLabel = rightLabel;
    rightLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [_self.contentView addSubview:rightLabel];

    UILabel *leftLabel = [[UILabel alloc] init];
    _self.leftLabel = leftLabel;
    leftLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [_self.contentView addSubview:leftLabel];

    NSDictionary *views = NSDictionaryOfVariableBindings(rightLabel, leftLabel);

    NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"|-10-[leftLabel]-(>=10)-[rightLabel]-10-|" options:0 metrics:nil views:views];
    [_self.contentView addConstraints:horizontalConstraints];

    // center views vertically in super view
    NSLayoutConstraint *leftCenterYConstraint = [NSLayoutConstraint constraintWithItem:leftLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_self.contentView attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
    [_self.contentView addConstraint:leftCenterYConstraint];
    NSLayoutConstraint *rightCenterYConstraint = [NSLayoutConstraint constraintWithItem:rightLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_self.contentView attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
    [_self.contentView addConstraint:rightCenterYConstraint];

    // same height for both labels
    NSLayoutConstraint *sameHeightConstraint = [NSLayoutConstraint constraintWithItem:leftLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:rightLabel attribute:NSLayoutAttributeHeight multiplier:1 constant:0];
    [_self.contentView addConstraint:sameHeightConstraint];
}

+ (BOOL)requiresConstraintBasedLayout {
    return YES;
}

- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithReuseIdentifier:reuseIdentifier];
    MBTableDetailStyleFooterViewCommonSetup(self);
    return self;
}

@end
_

このクラスは、2つのセクションを持つtableViewの最初のセクションのフッターとして使用されます。最初のセクションには動的アイテムが含まれています。 2番目のセクションには1つの行しかなく、最初のセクションに新しいアイテムを追加するために使用されます。

最初のセクションにアイテムがない場合は、footerViewを非表示にします。したがって、最初の新しいアイテムを追加するときに、footerViewが表示されるようにセクションをリロードする必要があります。これをすべて行うコードは次のようになります。

_- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    if (indexPath.section == 1) {
        BOOL sectionNeedsReload = ([self.data count] == 0); // reload section when no data (and therefor no footer) was present before the add
        [self.data addObject:[NSDate date]];
        NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:[self.data count]-1 inSection:0];
        if (sectionNeedsReload) {
            [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
        }
        else {
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
        }
        [self configureFooter:(MBTableDetailStyleFooterView *)[tableView footerViewForSection:0] forSection:0];
    }
}

- (void)configureFooter:(MBTableDetailStyleFooterView *)footer forSection:(NSInteger)section {
    footer.leftLabel.text = @"Total";
    footer.rightLabel.text = [NSString stringWithFormat:@"%d", [self.data count]];
}

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
    MBTableDetailStyleFooterView *footer = nil;
    if (section == 0 && [self.data count]) {
        footer = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"Footer"];
        [self configureFooter:footer forSection:section];
    }
    return footer;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
    CGFloat height = 0;
    if (section == 0 && [self.data count]) {
        height = 20.0f;
    }
    return height;
}
_

本当に派手なものはありません。ただし、tableViewで_reloadSections:withRowAnimations:_が呼び出されるとすぐに、「制約を同時に満たすことができない」ため、例外がスローされます。

TableViewは、翻訳された自動サイズ変更マスク制約をフッターに追加しました。

_(
    "<NSLayoutConstraint:0x718a1f0 H:[UILabel:0x7189130]-(10)-|   (Names: '|':_UITableViewHeaderFooterContentView:0x7188df0 )>",
    "<NSLayoutConstraint:0x7189e30 H:[UILabel:0x71892c0]-(>=10)-[UILabel:0x7189130]>",
    "<NSLayoutConstraint:0x718a0a0 H:|-(10)-[UILabel:0x71892c0]   (Names: '|':_UITableViewHeaderFooterContentView:0x7188df0 )>",
    "<NSAutoresizingMaskLayoutConstraint:0x7591ab0 h=--& v=--& H:[_UITableViewHeaderFooterContentView:0x7188df0(0)]>"
)
_

_reloadSections:withRowAnimations:_をreloadDataの呼び出しに置き換えると、自動サイズ変更マスク制約が追加されず、すべてが正常に機能します。

興味深いのは、例外が制約を破ろうとしていることを教えてくれることです<NSLayoutConstraint:0x7189e30 H:[UILabel:0x71892c0]-(>=10)-[UILabel:0x7189130]>

しかし、その後の_configureFooter:forSection:_の呼び出しで制約をログに記録すると、この制約はまだ存在しますが、自動サイズ変更マスク制約はなくなります。

制約は、私が設定したものとまったく同じです。

_(
    "<NSLayoutConstraint:0x718a0a0 H:|-(10)-[UILabel:0x71892c0]   (Names: '|':_UITableViewHeaderFooterContentView:0x7188df0 )>",
    "<NSLayoutConstraint:0x7189e30 H:[UILabel:0x71892c0]-(>=10)-[UILabel:0x7189130]>",
    "<NSLayoutConstraint:0x718a1f0 H:[UILabel:0x7189130]-(10)-|   (Names: '|':_UITableViewHeaderFooterContentView:0x7188df0 )>",
    "<NSLayoutConstraint:0x718a3f0 UILabel:0x71892c0.centerY == _UITableViewHeaderFooterContentView:0x7188df0.centerY>",
    "<NSLayoutConstraint:0x718a430 UILabel:0x7189130.centerY == _UITableViewHeaderFooterContentView:0x7188df0.centerY>",
    "<NSLayoutConstraint:0x718a4b0 UILabel:0x71892c0.height == UILabel:0x7189130.height>"
)
_

この自動サイズ変更マスク制約はどこから来ていますか?どこに行くの?

私は何かが足りないのですか?初めて自動レイアウトを調べたのは1週間前のようだったので、これは完全に可能です。

20
Matthias Bauch

IOS9現在の実用的なソリューション

UITableViewHeaderFooterViewサブクラスに、次のコードを配置します。

- (void)setFrame:(CGRect)frame {
    if (frame.size.width == 0) {
        return;
    }

    [super setFrame:frame];
}

説明:

テーブルビューはヘッダービューのレイアウトを処理し、フレームを手動で操作することによって処理します(自動レイアウトがオンになっている場合でも可能です)。

ヘッダー/フッタービューにある幅の制約を調べると、2つあります。1つはスーパービュー(テーブルビュー)に含まれる幅で、もう1つはヘッダー/フッタービュー自体に含まれる幅です。

スーパービューに含まれる制約はNSAutoresizingMaskLayoutConstraintです。これは、テーブルビューがヘッダーを操作するためにフレームに依存するという景品です。ヘッダービューでtranslatesAutoresizingMaskIntoConstraintsをNOに切り替えると、外観が効果的に壊れます。

状況によっては、これらのヘッダー/フッタービューのフレームがゼロの幅に変更されるようです。私にとっては、行が挿入されてヘッダービューが再利用されたときでした。私の推測では、UITableViewコードのどこかで、アニメーションを使用していない場合でも、フレームをゼロ幅で開始することによってアニメーションの準備が行われます。

このソリューションはうまく機能し、スクロールのパフォーマンスに影響を与えないはずです。

24
Josh Bernfeld

私は先週この週に出くわした。

警告を削除する方法は、 必要な制約を999の優先度を持つように変更する です。これは修正ではなく回避策ですが、レイアウト中に例外がスローされ、キャッチされ、ログに記録されるのを回避します。

私にとってうまくいかなかったこと。

提案は set estimatedRowHeight です。 estimatedSectionHeaderHeightを設定しようとしましたが、役に立ちませんでした。 estimatedSectionFooterHeightを設定すると、不要な場所に空のフッターが作成されましたが、これは少し奇妙でした。

私も設定してみました translatesAutoresizingMaskIntoConstraints = NO; ヘッダーフッタービューとそのコンテンツビュー。どちらも警告を取り除きませんでした、そして、1つはレイアウトを完全に壊すことにつながりました。

7
Benjohn

ContentViewにラベルを1つ追加するだけで、同様の問題が発生しました。挿入してみてください

static void MBTableDetailStyleFooterViewCommonSetup(MBTableDetailStyleFooterView *_self) {
    _self.contentView.translatesAutoresizingMaskIntoConstraints = NO
    [...]
}

MBTableDetailStyleFooterViewCommonSetup関数の最初の行。私にとって、これはreloadSections:withRowAnimations:と連携して機能します。

更新:

また、すべての幅を使用するようにcontentViewに新しい制約を追加しました。

NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|"
                                                                          options:0
                                                                          metrics:nil
                                                                            views:@{@"contentView" : _self.contentView}];
[_self.contentView.superview addConstraints:horizontalConstraints];
3
DEAD10CC

迷惑なことに、UITableViewHeaderFooterViewは一定の制約を好まないようです。

| -10- [表示] -10- |

作成時にビューの幅がゼロであり、制約を満たすことができないためだと思います。この場合、少なくとも20pxの幅が必要になります。

私にとって、これを回避する方法は、定数の制約を次のようなものに変更することでした。

|-(<= 10)-[表示]-(<= 10)-|

これは、ゼロ幅のコンテンツを満たし、サイズ変更時に必要なマージンを与える必要があります。

2
Brett

とても奇妙!この@ josh-bernfieldに感謝します、これが私がiOS11.3用に書いたものです:

override var frame: CGRect {
    get {
        return super.frame
    }
    set {
        if newValue.width == 0 { return }
        super.frame = newValue
    }
}
2
coco

IOS 11では、できませんでしたUITableViewHeaderFooterViewを取得して正しく表示します。

問題は、UITableViewHeaderFooterViewが境界が空であると考えたときに制約を設定していたため、常に競合が発生することでした。

これが解決策です

override func layoutSubviews() {
    super.layoutSubviews()
    guard bounds != CGRect.zero else {
        return
    }
    makeConstraints()
}
2
SparkyRobinson

contentViewframeを更新する必要があり、警告は消えます。これはおそらく ITableViewCellのiOS7での自動レイアウト制約の問題

public class MyHeaderview: UITableViewHeaderFooterView {
    // Add views to contentView

    public override func layoutSubviews() {
        super.layoutSubviews()
        contentView.frame = bounds
    }
}
0
onmyway133

同様の問題があり、contentViewにUILabelが1つしかないため、Masonryを使用しました。

_titleLabel = ({
    UILabel *label = [MLBUIFactory labelWithType:MALabelTypeB];
    [self.contentView addSubview:label];
    [label mas_makeConstraints:^(MASConstraintMaker */make) {
        make.centerY.equalTo(self.contentView);
        make.left.equalTo(self.contentView).offset(8);
        make.right.equalTo(self.contentView).offset(-8);
    }];

    label;
});

それから私は警告を受けました:

(
"<MASLayoutConstraint:0x1742b2c60 UILabel:0x124557ee0.left == _UITableViewHeaderFooterContentView:0x12454c640.left + 8>",
"<MASLayoutConstraint:0x1742b3080 UILabel:0x124557ee0.right == _UITableViewHeaderFooterContentView:0x12454c640.right - 8>",
"<NSLayoutConstraint:0x174280780 _UITableViewHeaderFooterContentView:0x12454c640.width == 0>"
)
Will attempt to recover by breaking constraint 
<MASLayoutConstraint:0x1742b3080 UILabel:0x124557ee0.right == _UITableViewHeaderFooterContentView:0x12454c640.right - 8>

self.translatesAutoresizingMaskIntoConstraints = NO;を設定しようとしましたが、うまくいきませんでした。警告メッセージに焦点を合わせました。なぜ左の制約が機能するのか混乱していますが、_UITableViewHeaderFooterContentView:0x12454c640.width == 0を見たとき、おそらくそれがなぜ正しい制約が破壊制約であるのか、それから私はコードを変更しました:

_titleLabel = ({
    UILabel *label = [MLBUIFactory labelWithType:MALabelTypeB];
    [self.contentView addSubview:label];
    [label mas_makeConstraints:^(MASConstraintMaker */make) {
        make.centerY.equalTo(self.contentView);
        make.left.equalTo(self.contentView).offset(8);
        make.width.equalTo(@(SCREEN_WIDTH - 16));
    }];

    label;
});

右側の制約を幅の制約に置き換えたところ、警告が消えました。

警告を消す方法は他にもあります:self.contentView.translatesAutoresizingMaskIntoConstraints = NO;、しかしラベルのフレームが間違っています。

0
Meilbn

コンソールの警告は無視しても問題ないと思います。

コンソールログは、テーブルビューレイアウトプロセスの中間状態が原因であると思われます。

自動レイアウトコンテンツを含むフッター断面図を追加すると、コンソールにも不満足な制約の警告が表示されます。

以下のログから、問題の原因となる制約は_UITableViewHeaderFooterContentView:0x7ff2115778d0.height == 0である必要があります。

2018-08-23 16:01:27.036159-0700 Attendee[45370:2760310] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x60000029d7e0 UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide'.bottom == UIButton:0x7ff2115770a0.bottom + 20   (active)>",
    "<NSLayoutConstraint:0x604000481d60 UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide'.top == UIButton:0x7ff211576790'Forgot password?'.top   (active)>",
    "<NSLayoutConstraint:0x604000481c70 UIButton:0x7ff211576790'Forgot password?'.bottom == UIButton:0x7ff2115770a0.top - 8   (active)>",
    "<NSLayoutConstraint:0x60000029d600 'UIView-bottomMargin-guide-constraint' V:[UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide']-(8)-|   (active, names: '|':_UITableViewHeaderFooterContentView:0x7ff2115778d0 )>",
    "<NSLayoutConstraint:0x60000029da10 'UIView-Encapsulated-Layout-Height' _UITableViewHeaderFooterContentView:0x7ff2115778d0.height == 0   (active)>",
    "<NSLayoutConstraint:0x60000029d560 'UIView-topMargin-guide-constraint' V:|-(8)-[UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide']   (active, names: '|':_UITableViewHeaderFooterContentView:0x7ff2115778d0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000029d7e0 UILayoutGuide:0x6000003b98a0'UIViewLayoutMarginsGuide'.bottom == UIButton:0x7ff2115770a0.bottom + 20   (active)>

フッタービューのlayoutSubviewsをオーバーライドして、垂直レイアウトに影響する制約をダンプすると、次のようになります。

override func layoutSubviews() {
    super.layoutSubviews()

    print("after layout")
    dump(contentView.constraintsAffectingLayout(for: .vertical))
}

次の出力が得られます。

▿ 2 elements
  - <NSLayoutConstraint:0x600000297110 'UIView-Encapsulated-Layout-Height' _UITableViewHeaderFooterContentView:0x7ffc22d88c20.height == 101   (active)> #0
    - super: NSObject
  - <NSAutoresizingMaskLayoutConstraint:0x6000002971b0 h=--& v=--& 'UIView-Encapsulated-Layout-Top' _UITableViewHeaderFooterContentView:0x7ffc22d88c20.minY == 0   (active, names: '|':Attendee.SearchFormButtonFooter:0x7ffc22d20da0 )> #1
    - super: NSLayoutConstraint
      - super: NSObject

コンテンツビューの高さを0に設定する明示的なレイアウト制約はありません。おそらく、UITableViewレイアウトプロセスの中間状態で問題を引き起こしているのは、コンテンツビューの自動サイズ変更マスクだけです。

0
billibala

私はこの問題に遭遇しました、そして私が見つけた最良の解決策はinitにデフォルトのフレームを設定することです。そのフレームはとにかく変更されますが、0,0,0,0フレームで壊れる制約には役立ちます。

override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)        
        frame = CGRect(x: 0, y: 0, width: 100, height: 100)

        //... setup constraints...
}
0
Bessi

フッター自体でtranslatesAutoresizingMaskIntoConstraints = NOを呼び出す場所がわかりません。あなたがそれを作成するときにこれを行うべきですか?

- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithReuseIdentifier:reuseIdentifier];
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MBTableDetailStyleFooterViewCommonSetup(self);
    return self;
}
0
KHansenSF