Sortierung von CollectionView-Zellen geht NUR auf dem iPad verloren

  • Sortierung von CollectionView-Zellen geht NUR auf dem iPad verloren

    Ich habe einen merkwürdigen Effekt mit einem CollectionView (Universal App).

    Das iPad Storyboard habe leer erstellt und alle Inhalte vom iPhone Storyboard per Copy&Paste dort eingefügt.
    Die Eigenschaften der Views,Coltroller, Segues usw. sind 1:1 übernommen worden.
    Also aus meiner Sicht gleiche App mit gleich konfigurierten Storyboard.

    Wenn ich die App auf dem iPhone und dem iPad starte sieht sie grundlegend gleich aus und alle 12 Zellen des CollectionViews (Kalender) sind auf Basis der Datasource sauber von Januar bis Dezember sortiert.

    Beim Tap auf eine Monats-Zelle stoße ich mit "performSegueWithIdentifier" ein Push-Segue an, das auf einen Detailcontroller springt.
    Wenn ich den Detailcontroller über den "Zurück-Button" der NavigationControllers verlasse, dann ist die Sortierung der Monate auf dem iPhone immer in Ordnung, aber auf dem iPad immer völlig durcheinander. s.Screenshots
    Die Monate mischen sich auf dem iPad nach jedem verlassen des Detailcontrollers.

    Der Detailcontroller ist noch ein leerer TableViewController. Da passiert noch nichts mit den Daten.

    Zur Fehlersuche habe ich in allen meinen Klassen "respondsToSelector" überschrieben und lasse loggen was alles aufgerufen wird.
    Beim Verlassen des Detailcontrollers wird die Methode zum Neuzeichnen der Kalender Zellen nicht erneut aufgerufen.

    Ich habe echt keinen Plan mehr wo ich weitersuchen kann.
  • Die Datenquelle ist zur Zeit noch ein einfaches NSArray mit Werten von 1 bis 12.
    Das ist aber nun wirklich einfach :)

    Später wird es ein gefilterter Bereich aus dem EKEventStore, damit ich Tage mit Ereignissen noch ein bisschen bunt hinterlegen kann ..

    Ich glaube, dass mit das Standard-FlowLayout des CollectionViews diesen Streich spielt.
    Das Displayformat ist ja im Endeffekt der einzige echte Unterschied...

    Wenn der Detailcontroller geschlossen wird, dann stimmt die Ansicht während der "Dismiss Animation" noch.
    Sofort danach würfelt die CollectionView die Monate auf dem iPad kräftig durch...
  • JensW_2000 schrieb:

    Die Datenquelle ist zur Zeit noch ein einfaches NSArray mit Werten von 1 bis 12.

    Mit Datenquelle meine ich Deine Implementierung von UICollectionViewDataSource, und dabei insbesondere die Methode collectionView:cellForItemAtIndexPath:.

    JensW_2000 schrieb:

    Ich glaube, dass mit das Standard-FlowLayout des CollectionViews diesen Streich spielt.

    Das glaube ich nicht. Ich vermute eher eine fehlerhafte Wiederverwendung der Zellen oder fehlerhaftes Neuzeichnen. Das Flowlayout stellt die Zellen in der richtigen Reihenfolge dar.
    „Meine Komplikation hatte eine Komplikation.“
  • Jetzt wird es glaube ich etwas kompliziert.
    Ich entwickle mit Qxygene. Das ist Pascal Code, der gegen die Objc Runtime compiliert wird. (Der ByteCode des Kompilats ist 100% identisch mit dem, den Xcode auch erstellen würde).

    collectionview:cellForItemAtIndexPath ist wir folgt implementiert...


    (ich kommentiere das mal stark, phonetisch entspricht die Syntax der ObjC "." Notation)

    Quellcode

    1. method YearDataViewController.collectionView(collectionView: UICollectionView) cellForItemAtIndexPath(indexPath: NSIndexPath): UICollectionViewCell;
    2. begin
    3. // hole denPointer der Zelle mit Identifier X für den IndexPath Y und speichere die Referenz der Zelle in den Funktions-Rückgabewert "result"
    4. // result ist der Standard-Rückgabewert einer Funktion, und muss daher nicht explizit mit "return result" ausgegeben werden.
    5. result := collectionView.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER) forIndexPath(indexPath);
    6. // caste "result" in meine "custom cell class" und setze die Werte der public properties, die die Zelle braucht um den Monatskalender zu zeichnen..
    7. (result as YearDataViewMonthCell).cellMonth := indexPath.row+1;
    8. (result as YearDataViewMonthCell).cellYear := pageYear;
    9. (result as YearDataViewMonthCell).collectionView := collectionView;
    10. (result as YearDataViewMonthCell).isPortrait := isPortrait;
    11. end;
    Alles anzeigen
  • JensW_2000 schrieb:

    Jetzt wird es glaube ich etwas kompliziert.
    Ich entwickle mit Qxygene. Das ist Pascal Code, der gegen die Objc Runtime compiliert wird. (Der ByteCode des Kompilats ist 100% identisch mit dem, den Xcode auch erstellen würde).

    Keine Sorge, als ich noch klein war, habe ich auch mal in Pascal entwickelt. +grusel+ ;)

    Kann es vielleicht sein, dass die Zelle nicht neu gezeichnet wird, wenn Du ihr einen Monat (und ein neues Jahr) zuweist? Rufst Du irgendwo setNeedsDisplay auf?
    „Meine Komplikation hatte eine Komplikation.“
  • Keine Sorge, als ich noch klein war, habe ich auch mal in Pascal entwickelt. +grusel+ ;)

    Oxygene ist das "Not Your Daddy's Pascal". Hat Sprachfeautures von denen C#'per, Java Entwickler, Delphi Menschen (wie ich) und auch ObjC'ler nachts träumen können und compiliert gegen .Net, WinRT, Java und ObjC (cocoa).
    Ich finds super. =)

    setNeedsDisplay rufe ich nicht explizit auf.
    Dafür habe ich "hasAmbigousLayout" der Zelle überschrieben und gebe dort immer "True" zurück.
    Vorher gab es den Effekt, dass bein Wechseln der Orientation immer nur die vollstänfig angezeigten Zellen neu gezeichnet wurden und die Inhalte der verbliebenen Zellen nicht richtig skaliert waren.

    Laut "respondsToSelector" wird cellForItemAtIndexPath beim verlassen des Detailcontrollers nicht durchlaufen.
    Da wird offensichtlich nur der DetailController vom "View-Stapel" geworfen und das darunterliegende Jahr "wie vorhanden" angezeigt.

    Was ich nicht verstehe ist:
    - Warum gibt es den Fehler nur auf den iPad?
    - Warum verschieben sich die Zellen erst nach der Dismiss Animation? Laut respondsToSelector werden dort nur folgende Sachen versucht aufzurufen:

    Quellcode

    1. YearDataViewController collectionView:layout:sizeForItemAtIndexPath:
    2. YearDataViewController collectionView:layout:referenceSizeForHeaderInSection:
    3. YearDataViewController collectionView:layout:referenceSizeForFooterInSection:
    4. YearDataViewController collectionView:layout:insetForSectionAtIndex:
    5. YearDataViewController collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
    6. YearDataViewController collectionView:layout:minimumLineSpacingForSectionAtIndex:
    7. YearDataViewController _collectionView:layout:flowLayoutRowAlignmentOptionsForSection:
    8. YearDataViewController collectionView:layout:sizeForItemAtIndexPath:
    9. YearDataViewController collectionView:layout:referenceSizeForHeaderInSection:
    10. YearDataViewController collectionView:layout:referenceSizeForFooterInSection:
    11. YearDataViewController collectionView:layout:insetForSectionAtIndex:
    12. YearDataViewController collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
    13. YearDataViewController collectionView:layout:minimumLineSpacingForSectionAtIndex:
    14. YearDataViewController _collectionView:layout:flowLayoutRowAlignmentOptionsForSection:
    Alles anzeigen


    Davon überschreibe ich nur "sizeForItemAtIndexPath".
    Die Metohode gibt mir je nach Orientation eine passende Zellengröße zurück, damit im Hochformat 3x4 Zellen aufs Diaplay passen und im Landscape 4 Zellen nebeneinander.
  • JensW_2000 schrieb:

    Oxygene ist das "Not Your Daddy's Pascal". Hat Sprachfeautures von denen C#'per, Java Entwickler, Delphi Menschen (wie ich) und auch ObjC'ler nachts träumen können und compiliert gegen .Net, WinRT, Java und ObjC (cocoa).

    Danke, jetzt ist mir schlecht. +scnr+

    Die ganzen Pascal-Nachfolger finde ich übrigens noch schlimmer, weil sie das einzig Gute an Pascal auch noch kaputt machen.

    JensW_2000 schrieb:

    setNeedsDisplay rufe ich nicht explizit auf.
    Dafür habe ich "hasAmbigousLayout" der Zelle überschrieben und gebe dort immer "True" zurück.

    Das scheint eine nicht Cocoa-Erweiterung zu sein. Prinzipiell besteht zwischen Layout und Darstellung (Display) ein unterschied. Neues Layout impliziert nicht neues Zeichnen. Erzwinge mal ein Neuzeichnen über setNeedsDisplay.

    JensW_2000 schrieb:

    Was ich nicht verstehe ist:
    - Warum gibt es den Fehler nur auf den iPad?

    Durch die andere Displaygröße hast Du eine andere Wiederverwendung der Zellen.

    JensW_2000 schrieb:

    Davon überschreibe ich nur "sizeForItemAtIndexPath".

    Das hat nichts mit Deinem Problem zu tun. Genauso wenig wie die von Dir mitgelogten Methodenaufrufe. Verwende lieber einen Debugger.

    Du kannst Dir ja mal die Positionen mit den Monats- und Jahre
    „Meine Komplikation hatte eine Komplikation.“
  • Hast Du die Doku zu hasAmbigousLayout gelesen?

    "This method should only be used for debugging constraint-based layout. No application should ship with calls to this method as part of its operation."
    Xcode 4 sucks – „,Multiple exclamation marks‘, he went on, shaking his head, are a sure sign of a diseased mind.‘“ (Terry Pratchett 1992: Eric)

    "Wir ordnen und befehlen hiermit allen Ernstes, dass die Advocati wollene schwarze Mäntel, welche bis unter das Knie gehen, unserer Verordnung gemäß zu tragen haben, damit man die Spitzbuben schon von weitem erkennt." (Friedrich Wilhelm I., Soldatenkönig)
  • Um das noch einmal zu klären:

    Das CollectionView verwendet Zellen immer wieder. Beim ersten Aufruf erzeugst es natürlich 12 neue Zellen für Deinen Kalender, denn es gibt ja noch keine. Diese Zellen sollten eben im cellForItemAtIndexPath erzeugt werden. Das machst du aber gar nicht. Du setzt nur ein paar Parameter anhand derer die Zelle dann nachher weiß wie sie sich zeichnen soll.

    Beim zweiten Aufruf passiert es jetzt, dass die CollectionView die Celle für Januar anfragt, sie durch das dequeueReusableCellWithReuseIdentifier die Zelle von September gekommt. Dort werden dann zwar die Parameter so umgesetzt, dass er beim Zeichnen daraus einen JAnuar machen soll aber genau das ist ja der Vorteil der Reuse Zellen, dass sie eben nur neu gezeichnet werden wenn es nötig ist. Durch das Umsetzen von ein paar Parametern entsteht für das System aber keine Notwendigkeit die View neu zu zeichen.
    Deshalb brauchst Du dringend einen setNeedsDisplay

    Gruß

    Claus
    2 Stunden Try & Error erspart 10 Minuten Handbuchlesen.

    Pre-Kaffee-Posts sind mit Vorsicht zu geniessen :)
  • Das klingt irgentwie logisch, aber verstehen tue ich es trotzdem nicht so ganz.
    Wenn ich mir die ObjC Implementierung anschaue, dann mache ich, -wenn ich das richtig interpretiere- nichts anderes.

    Quellcode

    1. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
    2. {
    3. UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    4. // erstelle einen Pointer auf eine Variable cell von Typ "UICollectionViewCell" und weise der Variablen den Pointer der "Reusable Cell" mit dem PathIndex "indexPath" des CollectionViews zu.
    5. // das entspricht bei mir result := collectionView.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER) forIndexPath(indexPath);
    6. //Die Zelle wird vom CollectionView beim ersten Zugriff (oder wenn nötig) neu erstellt.
    7. cell.backgroundColor = [UIColor colorWithPatternImage:[self.results objectAtIndex:indexPath.row]];
    8. // setze ein paar Eigenschaften der cell Instanz
    9. return cell;
    10. // gebe als Result den Pointer auf die "ggf. erstellte" und konfigurierte Zelle zurück
    11. // Das ist bei mir (der unnötige Befehl) exit(result). Result wird bei allen Funktionen als automatisch zurückgegeben, wenn diese nicht zuvor per exit(was anderes) oder ObjC: (return cell) beendet wurde.
    12. }
    Alles anzeigen

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von JensW_2000 ()

  • Doch, da setzt du die cell backgroundcolor. Diese ist dem System (Da es ja eine Systempropertie ist) als View verändernt bekannt und das System macht automatisch einen setNeedDisplay. Woher soll aber das System wissen, dass Deine Properties cellMonth etc den Inhalt der View verändern?

    Gruß

    Claus
    2 Stunden Try & Error erspart 10 Minuten Handbuchlesen.

    Pre-Kaffee-Posts sind mit Vorsicht zu geniessen :)
  • Gefunden... :whistling:

    Aber vorher nochmal kurz zum Verständnis.
    Ich rufe setNeedsDisplay jetzt für jede Zelle in cellForItemAtIndexPath und sizeForItemAtIndexPath auf.
    Damit ist sicher, dass setNeedsDisplay beim Erstellen der Zelle und beim Ändern der Orientation für jede Zelle mindestens einmal aufgerufen wird.
    Ist das zu oft?

    Behoben hat es das Problem allerdings nicht, obwohl ihr mit dem Reuse Problem beide richtig lagt.

    In der meiner Cell Class hatte ich die Methode "prepareForReuse" überschrieben.
    Dort hatte ich meine "Monats-KalenderView" (die auf dem ContentView liegt) auf nil gesetzt, damit ARC die erstellten Instanzen vor dem Wiederverwenden der Zelle wegwerfen kann.

    Seitdem ich das "bereinigen" weglasse, funktioniert auf dem iPhone und dem iPad alles wie es soll.

    Warum sich das prepareForReuse nur auf das iPad negativ ausgewirkt hat, habe ich leider immer noch nicht verstanden...
  • Klappt super.
    Ich habe jetzt alle unnötigen setNeedDisplay und collectionView.performUpdate Aufrufe rausgeworfen.
    setNeedDisplay setze ich nur noch in den Settern der Zellenklasse und collectionView.PerformUpdates muss nur noch im NavigationControllerDelegate und bei Orientation Änderung aufgerufen werden.
    Sehr übersichtlich...

    Danke euch beiden..