2016-09-26 2 views
1

Je travaille sur un projet qui inclut un outil d'annotation permettant aux utilisateurs de «dessiner» des documents avec des gestes au doigt ou avec un crayon. Naturellement, je suis désireux de mettre en œuvre annuler/refaire pour les chemins tracés.Implémentation de la fonction de restauration avec UndoManager dans l'application de dessin Swift 3

Ma mise en œuvre pour l'application de dessin est relativement conventionnelle. Ce que l'utilisateur voit sur l'écran est la combinaison d'une image bitmap mise en cache (un instantané de tous les chemins qui ont été dessinés avant l'actuel) avec un rendu "en direct" du chemin courant (un UIBezierPath). Lorsque touchesEnded est déclenchée, le nouveau chemin est ajouté au bitmap.

J'ai été en mesure d'implémenter l'annulation avec relativement peu de problèmes. J'ai créé un undoManager standard pour la classe:

let myUndoManager : UndoManager = { 
    let mUM : UndoManager = UndoManager() 
    mUM.levelsOfUndo = 6 
    return mUM 
}() 

La fonction qui est appelée à la fin de touchesEnded pour rendre le nouveau chemin en cache est appelé DrawBitmap. Au début de cette fonction, en supposant qu'il existe un chemin précédent en cache et avant de dessiner le nouveau, j'inscrire l'action undo suivante avec le gestionnaire de undo:

let previousCachedPath : UIImage = self.cachedPath 
self.myUndoManager.registerUndo(withTarget: self, selector: #selector(self.setBitmap(_:)), object: previousCachedPath) 

SetBitmap (_ previousCachedPath: UIImage) est une fonction qui réinitialise le bitmap affiché à l'image fournie.

J'ai des boutons annuler/rétablir liés respectivement aux méthodes undo() et redo(). Mis à part une certaine logique dictant quand ces boutons devraient être actifs (pour s'assurer que vous ne pouvez pas appuyer sur Annuler quand rien n'a été dessiné, etc), ils appellent simplement myUndoManager.undo() et myUndoManager.redo() respectivement:

func undo() -> Void { 
    guard self.myUndoManager.canUndo else { return } 
    self.myUndoManager.undo() 
    if !self.redoButton.isEnabled { 
     self.redoButton.isEnabled = true 
    } 
    if !self.myUndoManager.canUndo { 
     self.undoButton.isEnabled = false 
    } 

    self.setNeedsDisplay() 
} 

func redo() -> Void { 
    guard self.myUndoManager.canRedo else { return } 
    self.myUndoManager.redo() 
    if !self.undoButton.isEnabled { 
     self.undoButton.isEnabled = true 
    } 
    if !self.myUndoManager.canRedo { 
     self.redoButton.isEnabled = false 
    } 

    self.setNeedsDisplay() 
} 

Comme je l'ai mentionné, annuler fonctionne parfaitement avec les six niveaux spécifiés d'annulation. Cependant, il me manque clairement quelque chose avec refaire. Mon espoir initial était que le undoManager transfère automatiquement les tâches d'annulation de la pile d'annulation à la pile de rétablissement quand l'annulation est appelée, mais cela ne se produit manifestement pas.

Je l'ai déjà recherché des réponses, et je pense que le plus proche de ce que je dois peut-être utiliser récursion mutuelle par:

Using NSUndoManager, how to register undos using Swift closures

Cependant, je n'ai pas été en mesure de faire ce travail. Toute aide donc appréciée!

+0

L'astuce est que vous devriez avoir juste la méthode _one_ et à la fois annuler et refaire l'appeler. L'annulation d'un rétablissement est une annulation. L'annulation d'une annulation est un refaire. Cela pourrait vous aider à lire le chapitre de mon livre: http://www.apeth.com/iOSBook/ch39.html – matt

+0

Merci - Je vais vous lire ce matin et vous laisser savoir comment je m'entends! – Sparky

+0

@matt Merci beaucoup, a parfaitement fonctionné. Comme vous l'avez suggéré, j'ai tout combiné en une seule méthode, dans ce cas setBitmap (_ :). Je l'ai eu à travailler avec l'une des méthodes que vous avez décrites. J'ai posté ma solution au cas où cela aiderait les autres. – Sparky

Répondre

4

Grâce à l'aide de @matt, j'ai résolu ce problème en mettant tout dans la fonction setBitmap (_ :). Pour essayer de mieux comprendre les choses, j'implémenté à la fois la registerUndo (withTarget: sélecteur :) approche:

func setBitmap(_ toCachedPath : UIImage) -> Void { 
    self.myUndoManager.registerUndo(withTarget: self, selector: #selector(self.setBitmap(_:)), object: self.cachedPath) 

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0) 
    toCachedPath.draw(at: CGPoint.zero)  
    self.cachedPath = UIGraphicsGetImageFromCurrentImageContext() 
    UIGraphicsEndImageContext() 
} 

Et aussi avec la préparation (withInvocationTarget :) approche:

func setBitmap(_ toCachedPath : UIImage) -> Void { 
    if self.cachedPath != nil { 
     (self.rWUndoManager.prepare(withInvocationTarget: self) as AnyObject).setBitmap(self.cachedPath) 
    } 

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0) 
    toCachedPath.draw(at: CGPoint.zero) 
    self.cachedPath = UIGraphicsGetImageFromCurrentImageContext() 
    UIGraphicsEndImageContext() 
} 

espoir qui aide quelqu'un d'autre qui se grattaient la tête autant que moi.

0

En fait, vous pouvez utiliser la version de fermeture registerUndo(withTarget:handler:) pour que la fonction de reprise fonctionne également. Assurez-vous simplement que cela fonctionnerait d'abord avec le sélecteur registerUndo(withTarget:selector:object:), c'est-à-dire que vous feriez une fonction qui ne prend qu'un seul paramètre, comme dans votre réponse.Et vous pouvez remplacer la méthode de sélection avec la fermeture d'un:

func setBitmap(_ toCachedPath : UIImage) -> Void { 
    let oldCachedPath = cachedPath // For not referencing it with `self` in the closure. 
    myUndoManager.registerUndo(withTarget: self) { 
     $0.setBitmap(oldCachedPath) 
    } 

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0) 
    toCachedPath.draw(at: CGPoint.zero)  
    self.cachedPath = UIGraphicsGetImageFromCurrentImageContext() 
    UIGraphicsEndImageContext() 
} 

Je suppose que la version de fermeture est juste une reconditionner de la version de sélection qui ne reconnaît qu'une seule méthode et un paramètre d'entrée, donc nous avons encore écris comme ça pour que ça marche.