wake-up-neo.com

Einen View Controller aus UIPageViewController entfernen

Es ist seltsam, dass es keinen einfachen Weg gibt, dies zu tun. Stellen Sie sich das folgende Szenario vor:

  1. Sie haben einen Seitensicht-Controller mit 1 Seite.
  2. Fügen Sie eine weitere Seite hinzu (insgesamt 2) und blättern Sie zu dieser Seite.
  3. Was ich will, ist Wenn der Benutzer zur ersten Seite zurückblättert, wird die zweite Seite jetzt entfernt und freigegeben, und der Benutzer kann nicht mehr zu dieser Seite zurückkehren.

Ich habe versucht, den View-Controller als untergeordneten View-Controller zu entfernen, nachdem der Übergang abgeschlossen ist, aber ich kann immer noch zu der leeren Seite zurückblättern (die Seitenansicht wird dadurch nicht "angepasst").

Kann ich das machen?

44
Snowman

Während die Antworten hier alle informativ sind, gibt es eine alternative Möglichkeit, das Problem zu lösen.

UIPageViewController navigiert mit Scroll-Übergangsstil zu falscher Seite

Als ich zum ersten Mal nach einer Antwort auf dieses Problem gesucht habe, hat mich die Art und Weise, in der ich meine Suche formulierte, an dieser Frage interessiert, und nicht an der, die ich gerade verlinkt habe. Daher fühlte ich mich verpflichtet, eine Antwort auf diese andere Frage zu posten. jetzt, wo ich es gefunden habe, und auch ein wenig ausführlicher.

Das Problem wird von matt hier ziemlich gut beschrieben: 

Dies ist tatsächlich ein Fehler in UIPageViewController. Sie tritt nur mit dem Bildlaufstil (UIPageViewControllerTransitionStyleScroll) und nur mit .__ auf. nach dem Aufruf von setViewControllers: direction: animated: completion: mit animiert: JA. Daher gibt es zwei Problemumgehungen:

Verwenden Sie keine UIPageViewControllerTransitionStyleScroll.

Wenn Sie setViewControllers: direction: animated: completion: aufrufen, verwenden Sie nur animiert: NEIN.

Um den Fehler klar zu sehen, rufen Sie setViewControllers: direction: animated: completion: und dann in der Schnittstelle (als Benutzer), navigieren Sie nach links (zurück) zur vorherigen Seite manuell. Sie navigieren zurück zur falschen Seite: nicht zur vorherigen Seite überhaupt, aber die Seite, auf der Sie sich befanden, als setViewControllers: direction: animated: completion: wurde aufgerufen.

Der Grund für den Fehler scheint darin zu liegen, dass bei Verwendung der Bildlaufleiste In diesem Stil führt UIPageViewController eine Art internes Caching aus. Somit, nach dem Aufruf von setViewControllers: direction: animated: completion:, Der interne Cache kann nicht gelöscht werden. Es denkt, es wisse was Vorherige Seite ist. Wenn der Benutzer also nach links zum .__ navigiert. Auf der vorherigen Seite kann UIPageViewController die dataSource .__ nicht aufrufen. Methode pageViewController: viewControllerBeforeViewController: oder ruft es mit dem falschen aktuellen View Controller auf.

Dies ist eine gute Beschreibung, nicht ganz das in dieser Frage erwähnte Problem, aber sehr nahe. Beachten Sie die Zeile, wenn Sie setViewControllers mit animated:NO ausführen. Sie werden den UIPageViewController zwingen, seine Datenquelle erneut abzufragen, wenn der Benutzer das nächste Mal mit einer Geste schwenkt, da er nicht mehr "weiß, wo er sich befindet" oder welche Ansichtssteuerungen sich neben seiner aktuellen Position befinden Controller anzeigen.

Dies funktionierte jedoch nicht für mich, da ich manchmal die PageView programmgesteuert um mit einer Animation verschieben musste.

Mein erster Gedanke war also, setViewControllers mit einer Animation aufzurufen, und dann im Completion-Block die Methode erneut mit dem jetzt angezeigten View-Controller aufzurufen, jedoch ohne Animation. Der Benutzer kann also schwenken, gut, aber dann rufen wir die Methode erneut auf, damit die Seitenansicht zurückgesetzt wird.

Als ich das versuchte, bekam ich leider seltsame "Assertions-Fehler" vom Page-View-Controller. Sie sehen ungefähr so ​​aus:

*** Assertionsfehler in - [UIPageViewController queueingScrollView: ...

Da ich nicht genau wusste, warum dies geschah, ging ich zurück und begann mit Jais Antwort als Lösung. Ich erstellte einen völlig neuen UIPageViewController, schob ihn auf einen UINavigationController und holte dann den alten heraus. Gross, aber es funktioniert - meistens. Ich habe festgestellt, dass ich immer noch gelegentliche Assertionsfehler vom UIPageViewController bekomme, wie hier:

*** Assertionsfehler in - [UIPageViewController queuingScrollView: didEndManualScroll: toRevealView: direction: animated: didFinish: didComplete:], /SourceCache/UIKit_Sim/UIKit-2380.17/UIPageViewController.m:1820 $ 1 = 154507824 Kein Sichtcontroller, der sichtbare Sicht verwaltet>

Und die App stürzt ab. Warum? Nun, auf der Suche fand ich diese andere Frage , die ich oben erwähnt habe, und insbesondere die akzeptierte Antwort die meine ursprüngliche Idee befürwortet, einfach setViewControllers: animated:YES und dann, sobald der Aufruf von setViewControllers: animated:NO abgeschlossen ist, mit dem gleichen Aufruf aufzurufen Sehen Sie sich die Controller an, um den UIPageViewController zurückzusetzen, aber er hatte das fehlende Element: den Code in der Hauptwarteschlange zurückrufen! Hier ist der Code:

__weak YourSelfClass *blocksafeSelf = self;     
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished){
            if(finished)
            {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [blocksafeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];// bug fix for uipageview controller
                });
            }
        }];

Beeindruckend! Der einzige Grund, warum dies tatsächlich für mich sinnvoll war, liegt darin, dass ich die WWDC 2012-Sitzung 211, Erstellen gleichzeitiger Benutzeroberflächen unter iOS (verfügbar/hier -/mit einem Dev-Konto) gesehen habe. Ich erinnere mich jetzt daran, dass der Versuch, Datenquellenobjekte zu ändern, von denen UIKit-Objekte (wie beispielsweise UIPageViewController) abhängen, und dies in einer sekundären Warteschlange zu tun, einige unangenehme Abstürze verursachen kann.

Was ich noch nie besonders dokumentiert gesehen habe, aber jetzt annehmen muss, dass der Beendigungsblock für eine Animation in einer sekundären Warteschlange ausgeführt wird, ist nicht die Hauptwarteschlange. Der Grund, warum UIPageViewController kreischte und Assertionsfehler auslöste, sowohl beim ersten Aufruf von setViewControllers animated:NO im Beendigungsblock von setViewControllers animated:YES als auch jetzt, da ich einfach einen UINavigationController verwende, um einen neuen UIPageViewController zu drücken (jedoch wieder in Der Beendigungsblock von setViewControllers animated:YES) ist darauf zurückzuführen, dass sich alles in dieser sekundären Warteschlange befindet.Aus diesem Grund funktioniert der Code oben perfekt, da Sie aus dem Block für die Animation der Animation kommen und ihn zurück in die Hauptwarteschlange senden, damit Sie die Streams nicht mit UIKit kreuzen. Brillant.

Jedenfalls wollte ich diese Reise teilen, falls jemand auf dieses Problem stößt.

BEARBEITEN: Schnelle Version hier , wenn jemand interessiert ist.

EDIT: Swift version here , if anyone's interested.

121
Matt Mc

Um die großartige Antwort von Matt Mc abzuschließen, könnte die folgende Methode zu einer Unterklasse von UIPageViewController hinzugefügt werden, um so die Verwendung von setViewControllers zu ermöglichen: direction: animated: completion: So sollte es verwendet werden, wenn der Fehler nicht vorhanden wäre.

- (void) setViewControllers:(NSArray*)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^)(BOOL))completion {

    if (!animated) {
        [super setViewControllers:viewControllers direction:direction animated:NO completion:completion];
        return;
    }

    [super setViewControllers:viewControllers direction:direction animated:YES completion:^(BOOL finished){

        if (finished) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [super setViewControllers:viewControllers direction:direction animated:NO completion:completion];
            });
        } else {
            if (completion != NULL) {
                completion(finished);
            }
        }
    }];
}

Rufen Sie einfach setViewControllers auf: direction: animated: completion: auf den Klassen/Unterklassen, die diese Methode implementieren, und es sollte wie erwartet funktionieren.

9
Lukas Kalinski

maq ist richtig. Wenn Sie den Bildlauf-Übergang verwenden, wird durch das Entfernen eines untergeordneten View-Controllers aus dem UIPageViewController die gelöschte "Seite" nicht auf dem Bildschirm angezeigt, wenn der Benutzer zu ihr navigiert. Wenn Sie interessiert sind, habe ich den untergeordneten View-Controller aus dem UIPageViewController entfernt. 

// deleteVC is a child view controller of the UIPageViewController
[deleteVC willMoveToParentViewController:nil];
[deleteVC.view removeFromSuperview];
[deleteVC removeFromParentViewController]; 

Der Ansichtscontroller deleteVC wird aus der childViewControllers-Eigenschaft des UIPageViewController entfernt, erscheint jedoch weiterhin auf dem Bildschirm, wenn der Benutzer zu ihm navigiert. 

Bis jemand, der klüger als ich ist, eine elegante Lösung findet, folgt hier eine Problemumgehung (es ist ein Hack - Sie müssen sich also fragen, ob Sie wirklich Seiten aus einem UIPageViewController entfernen müssen). 

Diese Anweisungen setzen voraus, dass jeweils nur eine Seite angezeigt wird. 

Nachdem der Benutzer auf eine Schaltfläche geklickt hat, die angibt, dass er die Seite löschen möchte, navigieren Sie mit der Methode setViewControllers: direction: animated: completion: zur nächsten oder vorherigen Seite. Natürlich müssen Sie dann den Inhalt der Seite aus Ihrem Datenmodell löschen. 

Als Nächstes (und hier ist der Hack) erstellen und konfigurieren Sie einen brandneuen UIPageViewController und laden ihn in den Vordergrund (d. H. Vor dem anderen UIPageViewController). Stellen Sie sicher, dass der neue UIPageViewController die gleiche Seite anzeigt, die zuvor angezeigt wurde. Ihr neuer UIPageViewController holt frische View-Controller aus der Datenquelle. 

Zum Schluss entladen und zerstören Sie den im Hintergrund befindlichen UIPageViewController. 

Trotzdem hat maq eine wirklich gute Frage gestellt. Leider habe ich nicht genügend Reputationspunkte, um die Frage zu stimmen. Ach, wage zu träumen ... eines Tages werde ich 15 Reputationspunkte haben. 

6
bilobatum

Ich werde diese Antwort hier nur als Referenz für meine Zukunft angeben und wenn sie jemandem hilft - was ich letztendlich getan habe, war:

  1. Löschen Sie die Seite und fahren Sie mit der nächsten Seite fort
  2. Im Vervollständigungsblock von setViewControllers habe ich/init'ed einen neuen UIPageViewController mit den geänderten Daten erstellt (Element entfernt) und ohne Animation gedrückt, sodass sich nichts auf dem Bildschirm ändert (mein UIPageViewController ist in einem UINavigationController enthalten).
  3. Nachdem Sie den neuen UIPageViewController gedrückt haben, rufen Sie eine Kopie des viewControllers-Arrays des UINavigationController ab, und entfernen Sie den vorletzten View-Controller (der alte UIPageViewController).
  4. Es ist kein Schritt 4 - fertig!
5
Jai Govindani

Um dies zu verbessern. Sie sollten feststellen, ob pageView vor setViewControllers scrollt oder nicht.

var isScrolling = false

func viewDidLoad() {
...

  for v in view.subviews{
    if v.isKindOfClass(UIScrollView) {
      (v as! UIScrollView).delegate = self
    }
  }
}

func scrollViewWillBeginDragging(scrollView: UIScrollView){
    isScrolling = true
}

func scrollViewDidEndDecelerating(scrollView: UIScrollView){
    isScrolling = false
}

func jumpToVC{
    if isScrolling {  //you should not jump out when scrolling
        return
    }
    setViewControllers([vc], direction:direction, animated:true, completion:{[unowned self] (succ) -> Void in
        if succ {
            dispatch_async(dispatch_get_main_queue(), { () -> Void in
                self.setViewControllers([vc], direction:direction, animated:false, completion:nil)
            })
        }
    })
}
3
TonnyTao

Dieses Problem tritt auf, wenn Sie versuchen, viewControllers während der Swipe-Gesten-Animation zwischen viewControllers zu ändern. Um dieses Problem zu lösen, habe ich ein einfaches Flag gesetzt, um herauszufinden, wann sich der Page-View-Controller während des Übergangs befindet. 

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
    self.pageViewControllerTransitionInProgress = YES;
}

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
        self.pageViewControllerTransitionInProgress = NO;
}

Und wenn ich versuche, den View-Controller zu überprüfen, überprüfe ich, ob ein Übergang im Gange ist.

- (void)setCurrentPage:(NSString *)newCurrentPageId animated:(BOOL)animated {

    if (self.pageViewControllerTransitionInProgress) {
        return;
    }

    [self.pageContentViewController setViewControllers:@[pageDetails] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) {
    }

}
2
mientus

Ich lerne dies nur selbst, also nehmen Sie es mit einem Salzkorn mit, aber nach meinem Verständnis müssen Sie die Datenquelle des Pageviewcontroller ändern und nicht den Viewcontroller entfernen. Wie viele Seiten in einem Pageviewcontroller angezeigt werden, hängt von seiner Datenquelle ab, nicht von den Viewcontrollern.

1
user1459524
NSMutableArray *mArray = [[NSMutableArray alloc] initWithArray:self.userArray];
[mArray removeObject:userToBlock];
self.userArray = mArray;

UIViewController *startingViewController = [self viewControllerAtIndex:atIndex-1];
NSArray *viewControllers = @[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];

Ich hatte keine Zeit, die obigen Kommentare durchzulesen, aber das funktionierte für mich. Grundsätzlich entferne ich die Daten (in meinem Fall einen Benutzer) und gehe dann zur vorherigen Seite. Klappt wunderbar. Ich hoffe, das hilft denjenigen, die eine schnelle Lösung suchen.

0
Ibdakine

Ich hatte eine ähnliche Situation, in der ich wollte, dass der Benutzer jede Seite aus dem UIPageViewController "tippen und löschen" kann. Nachdem ich ein bisschen damit gespielt hatte, fand ich eine einfachere Lösung als die oben beschriebenen:

  1. Erfassen Sie den Seitenindex der "sterbenden Seite". 
  2. Prüfen Sie, ob die letzte Seite die letzte Seite ist.
    • Dies ist wichtig, da wir auf der letzten Seite nach links scrollen müssen (wenn wir Seiten "A B C" haben und C löschen, werden wir zu B scrollen).
    • Wenn es nicht die letzte Seite ist, werden wir nach rechts scrollen (wenn wir Seiten "A B C" haben und B löschen, werden wir zu C scrollen).
  3. Machen Sie einen vorübergehenden Sprung an einen "sicheren" Ort (idealerweise den letzten). Verwenden Sie animiert: NEIN, damit dies sofort geschieht.

    UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
    [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
    
  4. Löscht die ausgewählte Seite aus der Modell-/Datenquelle.

  5. Nun, wenn es die letzte Seite ist:

    • Passen Sie Ihr Modell an, um die linke Seite auszuwählen.
    • Holen Sie sich den Viewcontroller auf der linken Seite. Beachten Sie, dass DIESES IS DAS GLEICHE IST, das Sie in Schritt 3 abgerufen haben.
    • Dies ist wichtig, NACHDEM Sie die Seite aus der Datenquelle gelöscht haben, da diese aktualisiert wird.
    • Springe dazu, diesmal mit animiertem: JA. 

      jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
      [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
      
  6. Wenn es NICHT die letzte Seite war:

    • Passen Sie Ihr Modell an, um die Seite rechts auszuwählen.
    • Holen Sie sich den Viewcontroller auf der rechten Seite. Beachten Sie, dass dies nicht der ist, den Sie in Schritt 3 abgerufen haben. In meinem Fall werden Sie feststellen, dass es sich um dyingPageIndex handelt, da die sterbende Seite bereits aus dem Modell entfernt wurde.
    • Wieder ist es wichtig, dies auszuführen, NACHDEM Sie die Seite aus der Datenquelle gelöscht haben, da diese aktualisiert wird.
    • Springe dazu, diesmal mit animiertem: JA.

      jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex;
      [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
      
  7. Das ist es! Dies funktioniert gut in XCode 6.1.1.

Vollständiger Code unten. Dieser Code befindet sich in meinem UIPageViewController und wird über einen Delegaten der zu löschenden Seite aufgerufen. In meinem Fall war das Löschen der ersten Seite nicht zulässig, da sie andere Inhalte als die übrigen Seiten enthält. Natürlich müssen Sie Folgendes ersetzen: 

  • YourUIViewController: mit der Klasse Ihrer einzelnen Seiten. 
  • YourTotalNumberOfPagesFromModel: mit der Gesamtzahl der Seiten in Ihrem Modell 
  • [YourModel deletePage:] mit dem Code zum Löschen der sterbenden Seite aus Ihrem Modell

    - (void)deleteAViewController:(id)sender {
        YourUIViewController *dyingGroup = (YourUIViewController *)sender;
        NSUInteger dyingPageIndex = dyingGroup.pageIndex;
        // Check to see if we are in the last page as it's a special case.
        BOOL isTheLastPage = (dyingPageIndex >= YourTotalNumberOfPagesFromModel.count);
        // Make a temporary jump back - make sure to use animated:NO to have it jump instantly
        UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
        [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
        // Now delete the selected group from the model, setting the target
        [YourModel deletePage:dyingPageIndex];
        if (isTheLastPage) {
            // Now jump to the definitive controller. In this case, it's the same one, we're just reloading it to refresh the data source.
            // This time we're using animated:YES
            jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
            [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
        } else {
            // Now jump to the definitive controller. This reloads the data source. This time we're using animated:YES
            jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex];
            [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
        }
    }
    
0

Ich glaube, ich habe das Problem gelöst. Folgendes ist in meiner App passiert:

  1. Die UIViewController-Subklasseninstanz wurde aus UIPageViewController entfernt, indem sie aus dem Modell entfernt und die viewControllers-Eigenschaft von UIPageViewController auf einen anderen ViewController gesetzt wurde. Dies reicht aus, um die Arbeit zu erledigen. Es ist nicht erforderlich, einen Controller-Containment-Code auf diesem Controller auszuführen
  2. Der ViewController war verschwunden, aber ich konnte immer noch zu ihm scrollen, indem ich direkt in meinem Fall wischte. 
  3. Mein Problem war das. Ich habe einen benutzerdefinierten Gestenerkenner zu ViewController hinzugefügt, der von UIPageViewController angezeigt wird. Dieser Erkenner wurde als starke Eigenschaft auf dem Controller festgehalten, dem auch UIPageViewController gehörte.
  4. FIX: Bevor ich den Zugriff auf den ViewController verliere, stellte ich sicher, dass ich den gesamten verwendeten Speicher (Dealloc) ordnungsgemäß gelöscht und den Gestenerkenner entfernt habe
  5. Ihre Laufleistung kann variieren, aber ich sehe keinen Grund dafür, dass diese Lösung falsch ist. Wenn etwas nicht funktioniert, vermute ich zuerst meinen Code. :)
0
mdomans

In Swift 3.0 hat es folgendes getan 

fileprivate var isAnimated:Bool = false

override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewControllerNavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) {

        if self.isAnimated {
            delay(0.5, closure: { 
                self.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
            })
        }else {
                super.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
        }

    }


extension SliderViewController:UIPageViewControllerDelegate {

    func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
        self.isAnimated = true
    }

    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        self.isAnimated = finished
    }
}

Und hier die Verzögerungsfunktion 

func delay(_ delay:Double, closure:@escaping ()->()) {
    DispatchQueue.main.asyncAfter(
        deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
0
Eike