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.
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()
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.
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
}
}
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
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.
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
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
}
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.
Related
I have subclassed a UIView, and I am trying to access it's superview, but it is returned as a UIView instead of the type of my subclass.
Here is a Swift 4 Playground showing an oversimplification of the problem.
import UIKit
class SubUIView: UIView {}
var parent:SubUIView = SubUIView()
var child:SubUIView = SubUIView()
parent.addSubview(child)
var other:SubUIView = child.superview
How can I typecast child.superview into a SubUIView?
Currently, it throws this error:
Cannot convert value of type 'UIView?' to specified type 'SubUIView'
Previous Stakeoverflow answers on this topic were not clear to me, but the answer provided here was.
superview is a property on UIView that your view subclass inherits. If you want to cast the view returned from the call to superview you have mainly two options:
Force casting, where other's type will be SubUIView. If this operation fails – i.e. the super view isn't actually of type SubUIView, the application will crash. This is done with:
var other = child.superview as! SubUIView
You can also use as? instead of as!. This way the type for other will be SubUIView?, and if the casting fails, the value will be nil.
If you add type annotation to other, it'll have to match the result of the casting operation. As in:
var other: SubUIView = child.superview as! SubUIView
var other: SubUIView? = child.superview as? SubUIView
The type annotations are not needed though.
So I have a custom view touchableView inside a ViewController.
touchableView informs ViewController of changes in its properties through a delegate protocol (ViewController being the delegate).
What is the best method to change properties of touchableView from ViewController (so the other way around)?
Is there a way to create a two-way delegate relationship between two classes?
Simply:
Assuming that you are already have touchableView instance in the ViewController, you should be able to set -or get-/call its properties and methods.
For instance, assume that you have the following method in touchableView class:
class func fromNib() -> TouchableView {
return Bundle.main.loadNibNamed(String(describing: self), owner: nil, options: nil)![0] as! TouchableView
}
You should simply be able to:
let touchableView = TouchableView.fromNib()
thus:
// for example
touchableView.myProperty = "Hello"
I assumed that TouchableView has a string property called myProperty...
Remark:
For some reason, I suggest to implement some of property observers in TouchableView:
Property observers observe and respond to changes in a property’s
value. Property observers are called every time a property’s value is
set, even if the new value is the same as the property’s current
value.
They might be -somehow- useful to be implemented in your custom class. For clarity, let's consider -for instance- that if editing the value of myProperty should be effecting the background color of the view, it might be implemented as:
var myProperty: String = "Initial Value" {
willSet {
print("About to set value to: \(newValue)")
}
didSet {
backgroundColor = UIColor.red
}
}
Further Reading:
If you are looking for an advanced approach for two way bindings (Implementing MVVM), you might want to check this article.
I would also suggest to take a look at some frameworks that will be so helpful for such an approach, such as RxSwift, for a more simple framework, you might want to check ReactiveKit/Bond.
I am trying to achieve a singleton UIView instance in my code, so whenever I create an instance of 'MyView' the nib file will load for only once and then reuse it again:
class MyView : UIView {
#IBOutlet weak var someLabel: UILabel!
static var customeView : UIView = {
let view = Bundle.main.loadNibNamed(String(describing: MyView.self), owner: self, options: nil)?[0] as! UIView
return view
}()
convenience init() {
self.init(frame: CGRect(x: 0, y: 0, width: 576, height: 30))
let viewForOnce = MyView.customeView
viewForOnce.frame = bounds
}
}
The problem is that whenever I call the MyView.customeView it get crashed saying that 'this class is not key value coding-compliant', I think this is happening because of the 'owner: self' inside the computed property.
Any help will be appreciated.
The issue is most probably with your ReceiptView.xib. You should review it first.
Look for an IBOutlet, which is there in a xib, but not in your ReceiptView class and delete it.
Edit:
I meant with the singleton is loading the nib only once whenever I
create and instance of that class.
No, this is not going to work like that. Every time init is called, your computed property is also called and a new instance is created from xib every time.
As Carien van Zyl already mentioned, you are using self in a class var which corresponds to MyView class itself (or it's subclass if its called for a subclass), not an instance. Try passing nil as owner instead.
The whole technique is looking wrong to me. You should not use singleton pattern with UIView subclasses.
There is nothing wrong in calling loadNibNamed multiple times and create exactly the same instances. If you want to use the same instance multiple times in a view hierarchy, it's not possible since every view can have only one superview. In this case you should follow MVC pattern: create multiple MyView instances -> update model whenever you change something in a view and want those changes to be reflected elsewhere -> update another view using updated model.
customeView is a type property. Therefor, self inside of it, will reference MyView.self which is a class type. The class type do not hold the instance variables, which includes someLabel.
See Apple's documentation on Types
I have an app I'm working on that has various viewcontrollers that use the same constants.
ex.
let wrongAnswerBanner = UIImageView(image: UIImage(named: "torn_banner"))
I tried declaring the constant outside the viewcontrollers but whenever I try to call the constant by using self.
self.wrongAnswerBanner.hidden = false
I get the error: Value of type 'ViewController1' has no member 'wrongAnswerBanner'. How can I declare these constants without having to redeclare them within each individual viewcontroller?
You can declare the constant outside of a class scope and access it directly from any file inside the module.
File 1
let MyConstant = "MyConstant"
class A {
}
File 2
class B {
// use MyConstant directly (eg. print(MyConstant))
}
I usually do this for UITableViewCell identifiers. I declare them on top of my UITableViewCell subclass and use them on the ViewController file. It's worth nothing though (as other developers mentioned) that UIImageView might not be a good candidate for constants. You can also make use of Enums if that makes sense for your problem.
Just try to call the constant without self
Create a struct with constants:
struct Constant {
static let SomeConstant = "hey"
}
then you can get the value from any class by
let constant = Constant.SomeConstant
According to me you have to create one AppConstant.swift file in your application ,then just add this line in AppConstant.swift file
let wrongAnswerBanner = UIImageView(image: UIImage(named: "torn_banner"))
Then in any of controller you can easily access wrongAnswerBanner without using self
I have the below UINib extension method, I was wondering if I can set a delegate for the unarchived view
public class func decodeView<T:UIView>(nibName name:String,className classType:T.Type,delegate:AnyObject) -> T {
let nib = UINib(nibName: name)
let topLevelObjects = nib.instantiateWithOwner(nil, options: nil)
let view = topLevelObjects[0] as T
view.setTranslatesAutoresizingMaskIntoConstraints(false)
//check if view.delegate exists then view.delegate = delegate
return view
}
If you're asking if Swift supports reflection, TL;DR: you need to subclass from NSObject. Else you get limited info.
In this question, Does Swift support reflection? you get a more detailed discussion about the possibilities you have.
Once you have this part cleared, an example of how to obtain a list of properties can be found in this SO Answer
Although a quick & dirty way could be just to try and access the property (using KVC) and catch the exception if it fails. Swift does NOT support Try/Catch/Finally constructs, but this nice hack allows you to write code like:
SwiftTryCatch.try({
// try something
}, catch: { (error) in
println("\(error.description)")
}, finally: {
// close resources
})