53

Because objects are reference types, not value types, if you set a UIView equal to another UIView, the views are the same object. If you modify one you'll modifying the other as well.

I have an interesting situation where I would like to add a UIView as a subview in another view, then I make some modifications, and those modifications should not affect the original UIView. How can I make a copy of the UIView so I can ensure I add that copy as a subview instead of a reference to the original UIView?

Note that I can't recreate the view in the same way the original was created, I need some way to create a copy given any UIView object.

Jordan H
  • 52,571
  • 37
  • 201
  • 351

8 Answers8

140

You can make an UIView extension. In example snippet below, function copyView returns an AnyObject so you could copy any subclass of an UIView, ie UIImageView. If you want to copy only UIView you can change the return type to UIView.

//MARK: - UIView Extensions

extension UIView
{
    func copyView<T: UIView>() -> T {
        return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self)) as! T
    }
}

Example usage:

let sourceView = UIView()
let copiedView: UIView = sourceView.copyView()
Danny Bravo
  • 4,534
  • 1
  • 25
  • 43
Ivan Porkolab
  • 2,009
  • 3
  • 14
  • 17
23

You can't arbitrarily copy an object. Only objects that implement the NSCopying protocol can be copied.

However, there is a workaround: Since UIViews can be serialized to disk (e.g. to load from a XIB), you could use NSKeyedArchiver and NSKeyedUnarchiver to create a serialized NSData describing your view, then de-serialize that again to get an independent but identical object.

uliwitness
  • 8,532
  • 36
  • 58
  • @DannyBravo Where do you get that from? My UIView.h only declares conformance to NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem and CALayerDelegate. No NSCopying in there as far as I can see. – uliwitness Feb 24 '17 at 11:38
  • You are totally right, I got mixed up there for a bit. I'm removing my answer. – Danny Bravo Mar 01 '17 at 11:51
18

Update for iOS >= 12.0

Methods archivedData(withRootObject:) and unarchivedObject(with:) are deprecated as of iOS 12.0.

Here is an update to @Ivan Porcolab's answer using the newer API (since 11.0), also made more general to support other types.

extension NSObject {
    func copyObject<T:NSObject>() throws -> T? {
        let data = try NSKeyedArchiver.archivedData(withRootObject:self, requiringSecureCoding:false)
        return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? T
    }
}
David James
  • 2,430
  • 1
  • 26
  • 35
6

This answer shows how to do what @uliwitness suggested. That is, get an identical object by archiving it and then unarchiving it. (It is also basically what Ivan Porkolab did in his answer, but in a more readable format, I think.)

let myView = UIView()

// create an NSData object from myView
let archive = NSKeyedArchiver.archivedData(withRootObject: myView)

// create a clone by unarchiving the NSData
let myViewCopy = NSKeyedUnarchiver.unarchiveObject(with: archive) as! UIView

Notes

  • Unarchiving the data creates an object of type AnyObject. We used as! UIView to type cast it back to a UIView since we know that's what it is. If our view were a UITextView then we could type cast it as! UITextView.
  • The myViewCopy no longer has a parent view.
  • Some people mention some problems when working with UIImage. However, see this and this answer.

Updated to Swift 3.0

Community
  • 1
  • 1
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • I implemented this and for a second I thought it was working. But i'm copying an UIView that has subviews and the result i'm getting are the subviews, but not the view itself. I saw it debugging the view hierarchy. The view that im copying is an IBOutlet instead of `UIView()`. Does it sounds familiar? – Samuel Méndez Sep 29 '16 at 08:52
  • I'm sorry. When debugging a little more what is happening is that in the copied view I get `view.top = view.bottom` as a constraint and that's why i don`t see it in the view hierarchy. – Samuel Méndez Sep 29 '16 at 09:05
4

I think that you should link you UIView with a .nib and just create a new one.

Property will not be the same, but you keep appearance and methods.

Florian
  • 855
  • 7
  • 12
  • That's also a good approach. If you already have a XIB that you define the object in, and you're mostly concerned about setting it up correctly, and the attributes you change on it are few, you can just load the XIB a second time (you can have XIBs with a single free-floating view in them). – uliwitness Nov 25 '14 at 12:07
  • 1
    if it's xib based. – eric Sep 23 '16 at 00:56
1

An addition solution could be to just create a new UIView and then copy over any critical properties. This may not work in the OP's case, but it could very well work for other cases.

For example, with a UITextView, probably all you would need is the frame and attributed text:

let textViewCopy = UITextView(frame: textView.frame)
textViewCopy.attributedText = textView.attributedText 
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
-1

This is mostly just an update for iOS 12+

extension NSObject {
func copyObject<T:NSObject>() throws -> T? {
    let data = try NSKeyedArchiver.archivedData(withRootObject:self, requiringSecureCoding:false)
    return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? T
}
Pavel
  • 1
  • 2
-4

Additionally you can use this pattern to copy View controller view

let vc = UIViewController() 
let anotherVc = UIViewController()
vc.view = anotherVc.copyView()

You may need this for caching view controller or cloning.