I want to copy one UIView to another view without making it archive or unarchive.
Please help me if you have any solution.
I tried with by making an extension of UIView as already available an answer on Stack over flow. But its crashing when I pass the view with pattern Image Background color.
The code related to my comment below:
extension UIView
{
func copyView() -> UIView?
{
return NSKeyedUnarchiver.unarchiveObjectWithData(NSKeyedArchiver.archivedDataWithRootObject(self)) as? UIView
}
}
I've just tried this simple code in a Playground to check that the copy view works and it's not pointing the same view:
let originalView = UIView(frame: CGRectMake(0, 0, 100, 50));
originalView.backgroundColor = UIColor.redColor();
let originalLabel = UILabel(frame: originalView.frame);
originalLabel.text = "Hi";
originalLabel.backgroundColor = UIColor.whiteColor();
originalView.addSubview(originalLabel);
let copyView = originalView.copyView();
let copyLabel = copyView?.subviews[0] as! UILabel;
originalView.backgroundColor = UIColor.blackColor();
originalLabel.text = "Hola";
originalView.backgroundColor; // Returns black
originalLabel.text; // Returns "Hola"
copyView!.backgroundColor; // Returns red
copyLabel.text; // Returns "Hi"
If the extension wouldn't work, both copyView and originalView would have same backgroundColor and the same would happen to the text of the labels. So maybe there is the possibility that the problem is in other part.
Original Post
func copyView(viewforCopy: UIView) -> UIView {
viewforCopy.hidden = false //The copy not works if is hidden, just prevention
let viewCopy = viewforCopy.snapshotViewAfterScreenUpdates(true)
viewforCopy.hidden = true
return viewCopy
}
Updated for Swift 4
func copyView(viewforCopy: UIView) -> UIView {
viewforCopy.isHidden = false //The copy not works if is hidden, just prevention
let viewCopy = viewforCopy.snapshotView(afterScreenUpdates: true)
viewforCopy.isHidden = true
return viewCopy!
}
Related
How to remove subviews?
I am trying to integrate GIF by creating UIView and UIImageView programmatically.
It works fine to show GIF but when the function of hiding if is called, there is no response.
Here are the codes of both functions.
class CustomLoader: UIView {
static let instance = CustomLoader()
var viewColor: UIColor = .black
var setAlpha: CGFloat = 0.5
var gifName: String = ""
lazy var transparentView: UIView = {
let transparentView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
transparentView.backgroundColor = viewColor.withAlphaComponent(setAlpha)
transparentView.isUserInteractionEnabled = false
return transparentView
}()
lazy var gifImage: UIImageView = {
var gifImage = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 60))
gifImage.contentMode = .scaleAspectFit
gifImage.center = transparentView.center
gifImage.isUserInteractionEnabled = false
gifImage.loadGif(name: gifName)
return gifImage
}()
func showLoaderView() {
self.addSubview(self.transparentView)
self.transparentView.addSubview(self.gifImage)
self.transparentView.bringSubview(toFront: self.gifImage)
UIApplication.shared.keyWindow?.addSubview(transparentView)
}
func hideLoaderView() {
self.transparentView.removeFromSuperview()
}
}
A couple of thoughts:
I’d suggest you add a breakpoint or a logging statement in hideLoaderView and make sure you’re getting to that line.
You should make the init method to this class private to make sure you’re not calling hideLoaderView on some separate instance. When dealing with singletons, you want to make sure you can’t accidentally create another instance.
But I tested your code, and it works fine. Your problem probably rests with where and how you call this (and making init private, you might find where you might be using it inappropriately).
In the comments below, you said:
I simply call the function "CustomLoader().hideLoaderView()" Both are being called technically. What do you mean by "where I using it inappropriately?"
That is the root of the problem.
The CustomLoader() of CustomLoader().hideLoaderView() will create a new instance of CustomLoader with its own transparencyView, etc., which is precisely what the problem is. You’re not hiding the old view that was presented earlier, but trying to hide another one that you just created and was never displayed.
If you instead use that static, e.g. CustomLoader.instance.showLoaderView() and CustomLoader.instance.hideLoaderView(), then the problem will go away. Then you will be hiding the same view that your previously showed.
By the way, a few other unrelated observations:
If this is a singleton or shared instance, the convention would be to call that static property shared, not instance.
By the way, you aren’t using this CustomLoader as a UIView, so I’d not make it a UIView subclass. Don’t make it a subclass of anything.
You would obviously eliminate that self.addSubview(transparentView) line, too.
The bringSubview(toFront:) call is unnecessary.
You should avoid referencing UIScreen.main.bounds. You don’t know if your app might be in multitasking mode (maybe this isn’t an issue right now, but it’s the sort of unnecessary assumption that will cause problems at some later date). Just refer to the bounds of the UIWindow to which you’re adding this. You should also update this frame when you show this view, not when you create it (in case you changed orientation in the intervening time, or whatever).
By the way, using keyWindow is discouraged in iOS 13 and later, so you might eventually want to remove that, too.
When adding the gifImage (which I’d suggest renaming to gifImageView because it’s an image view, not an image), you should not reference the center of its superview. That’s the coordinate of the transparent view in its super view’s coordinate system, which could be completely different than the transparent view’s own coordinate system. In this case, it just happens to work, but it suggests a fundamental misunderstanding of view coordinate systems. Reference the bounds of the transparentView, not its center.
If you’re going to expose viewColor and setAlpha, you should pull the setting of the transparentView’s color out of the lazy initializer and into showLoaderView, at the very least. Right now, if you show the loader once, and then change the color, and try to show it again, you won’t see the new color.
The same issue applies with the gif image. So, I’d move that to the didSet observer.
Thus, pulling this all together:
class CustomLoader{
static let shared = CustomLoader()
private init() { }
var dimmingColor: UIColor = .black
var dimmingAlpha: CGFloat = 0.5
var gifName: String = "" { didSet { gifImage.loadGif(name: gifName) } }
lazy var transparentView: UIView = {
let transparentView = UIView()
transparentView.isUserInteractionEnabled = false
return transparentView
}()
lazy var gifImageView: UIImageView = {
var gifImage = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 60))
gifImage.contentMode = .scaleAspectFit
gifImage.isUserInteractionEnabled = false
return gifImage
}()
func showLoaderView() {
guard let window = UIApplication.shared.keyWindow else { return }
transparentView.frame = window.bounds
transparentView.backgroundColor = dimmingColor.withAlphaComponent(dimmingAlpha)
gifImageView.center = CGPoint(x: transparentView.bounds.midX, y: transparentView.bounds.midY)
transparentView.addSubview(gifImageView)
window.addSubview(transparentView)
}
func hideLoaderView() {
transparentView.removeFromSuperview()
}
}
Why you are using transparentView while you are have a CustomLoader instance view
Try to use this
class CustomLoader: UIView {
static let instance = CustomLoader()
var viewColor: UIColor = .black
var setAlpha: CGFloat = 0.5
var gifName: String = ""
init() {
super.init(frame: UIScreen.main.bounds)
backgroundColor = viewColor.withAlphaComponent(setAlpha)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var gifImage: UIImageView = {
var gifImage = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 60))
gifImage.backgroundColor = .red
gifImage.contentMode = .scaleAspectFit
gifImage.center = center
gifImage.isUserInteractionEnabled = false
gifImage.loadGif(name: gifName)
return gifImage
}()
func showLoaderView() {
addSubview(self.gifImage)
UIApplication.shared.keyWindow?.addSubview(self)
}
func hideLoaderView() {
removeFromSuperview()
}
}
I am unable to change the prompt color on my navigation bar. I've tried the code below in viewDidLoad, but nothing happens.
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white]
Am I missing something? Is the code above wrong?
I was able to make the prompt color white on iOS 11 was setting the barStyle to black. I set the other color attributes (like the desired background color) using the appearance proxy:
myNavbar.barStyle = UIBarStyleBlack; // Objective-C
myNavbar.barStyle = .black // Swift
It seems like you're right about this one. You need to use UIAppearance to style the prompt text on iOS 11.
I've filed radar #34758558 that the titleTextAttributes property just stopped working for prompt in iOS 11.
The good news is that there are a couple of workarounds, which we can uncover by using Xcode's view hierarchy debugger:
// 1. This works, but potentially changes *all* labels in the navigation bar.
// If you want this, it works.
UILabel.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).textColor = UIColor.white
The prompt is just a UILabel. If we use UIAppearance's whenContainedInInstancesOf:, we can pretty easily update the color the way we want.
If you look closely, you'll notice that there's also a wrapper view on the UILabel. It has its own class that might respond to UIAppearance...
// 2. This is a more precise workaround but it requires using a private class.
if let promptClass = NSClassFromString("_UINavigationBarModernPromptView") as? UIAppearanceContainer.Type
{
UILabel.appearance(whenContainedInInstancesOf: [promptClass]).textColor = UIColor.white
}
I'd advise sticking to the more general solution, since it doesn't use private API. (App review, etc.) Check out what you get with either of these two solutions:
You may use
for view in self.navigationController?.navigationBar.subviews ?? [] {
let subviews = view.subviews
if subviews.count > 0, let label = subviews[0] as? UILabel {
label.textColor = UIColor.white
label.backgroundColor = UIColor.red
}
}
It will be a temporary workaround until they'll fix it
More complicated version to support old and new iOS
func updatePromptUI(for state: State) {
if (state != .Online) {
//workaround for SOFT-7019 (iOS 11 bug - Offline message has transparent background)
if #available(iOS 11.0, *) {
showPromptView()
} else {
showOldPromptView()
}
}
else {
self.navigationItem.prompt = nil
if #available(iOS 11.0, *) {
self.removePromptView()
} else {
self.navigationController?.navigationBar.titleTextAttributes = nil
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor:UIColor.lightGray]
}
}
}
private func showOldPromptView() {
self.navigationItem.prompt = "Network Offline. Some actions may be unavailable."
let navbarFont = UIFont.systemFont(ofSize: 16)
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.font: navbarFont, NSAttributedStringKey.foregroundColor:UIColor.white]
}
private func showPromptView() {
self.navigationItem.prompt = String()
self.removePromptView()
let promptView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 18))
promptView.backgroundColor = .red
let promptLabel = UILabel(frame: CGRect(x: 0, y: 2, width: promptView.frame.width, height: 14))
promptLabel.text = "Network Offline. Some actions may be unavailable."
promptLabel.textColor = .white
promptLabel.textAlignment = .center
promptLabel.font = promptLabel.font.withSize(13)
promptView.addSubview(promptLabel)
self.navigationController?.navigationBar.addSubview(promptView)
}
private func removePromptView() {
for view in self.navigationController?.navigationBar.subviews ?? [] {
view.removeFromSuperview()
}
}
I suggest using a custom UINavigationBar subclass and overriding layoutSubviews:
- (void)layoutSubviews {
[super layoutSubviews];
if (self.topItem.prompt) {
UILabel *promptLabel = [[self recursiveSubviewsOfKind:UILabel.class] selectFirstObjectUsingBlock:^BOOL(UILabel *label) {
return [label.text isEqualToString:self.topItem.prompt];
}];
promptLabel.textColor = self.tintColor;
}
}
Basically I'm enumerating all UILabels in the subview hierarchy and check if their text matches the prompt text. Then we set the textColor to the tintColor (feel free to use a custom color). That way, we don't have to hardcode the private _UINavigationBarModernPromptView class as the prompt label's superview. So the code is be a bit more future-proof.
Converting the code to Swift and implementing the helper methods recursiveSubviewsOfKind: and selectFirstObjectUsingBlock: are left as an exercise to the reader 😉.
You can try this:
import UIKit
class ViewController: UITableViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updatePrompt()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updatePrompt()
}
func updatePrompt() {
navigationItem.prompt = " "
for view in navigationController?.navigationBar.subviews ?? [] where NSStringFromClass(view.classForCoder) == "_UINavigationBarModernPromptView" {
if let prompt = view.subviews.first as? UILabel {
prompt.text = "Hello Red Prompt"
prompt.textColor = .red
}
}
navigationItem.title = "This is the title (Another color)"
}
}
Moshe's first answer didn't work for me because it changed the labels inside of system VCs like mail and text compose VCs. I could change the background of those nav bars but that opens up a whole other can of worms. I didn't want to go the private class route so I only changed UILabels contained inside of my custom navigation bar subclass.
UILabel.appearance(whenContainedInInstancesOf: [NavigationBar.self]).textColor = UIColor.white
Try this out:->
navController.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor.rawValue: UIColor.red]
I've found next work around for iOS 11.
You need set at viewDidLoad
navigationItem.prompt = UINavigationController.fakeUniqueText
and after that put next thing
navigationController?.promptLabel(completion: { label in
label?.textColor = .white
label?.font = Font.regularFont(size: .p12)
})
extension UINavigationController {
public static let fakeUniqueText = "\n\n\n\n\n"
func promptLabel(completion: #escaping (UILabel?) -> Void) {
gloabalThread(after: 0.5) { [weak self] in
guard let `self` = self else {
return
}
let label = self.findPromptLabel(at: self.navigationBar)
mainThread {
completion(label)
}
}
}
func findPromptLabel(at view: UIView) -> UILabel? {
if let label = view as? UILabel {
if label.text == UINavigationController.fakeUniqueText {
return label
}
}
var label: UILabel?
view.subviews.forEach { subview in
if let promptLabel = findPromptLabel(at: subview) {
label = promptLabel
}
}
return label
}
}
public func mainThread(_ completion: #escaping SimpleCompletion) {
DispatchQueue.main.async(execute: completion)
}
public func gloabalThread(after: Double, completion: #escaping SimpleCompletion) {
DispatchQueue.global().asyncAfter(deadline: .now() + after) {
completion()
}
}
I have been search a while for this issue , I want my search bar display like BBC News App
I try all related method
for view in searchBar.subviews {
if view.isKindOfClass(NSClassFromString("UISearchBarBackground")!) {
view.removeFromSuperview()
break;
}
}
self.searchBar.tintColor = UIColor.clearColor()
self.searchBar.backgroundColor = UIColor.clearColor()
self.searchBar.translucent = true
here is my output
Am I miss something ??? Please Help me , thx !
Swift 3
To remove the background altogether, set backgroundImage to an empty image:
searchBar.backgroundImage = UIImage()
To set a custom background color, use barTintcolor property:
searchBar.barTintColor = .green
Thx all , I solve the question by setting the background image to 'nil' , which is a nonexistent image in my app
my final output
==================== Update Final Solution ====================
After read more documents . Finally I found a better solution ,
for subView in searchBar.subviews {
for view in subView.subviews {
if view.isKindOfClass(NSClassFromString("UINavigationButton")!) {
let cancelButton = view as! UIButton
cancelButton.setTitle("取消", forState: UIControlState.Normal)
cancelButton.setTitleColor(UIColor.whiteColor(), forState: .Normal)
}
if view.isKindOfClass(NSClassFromString("UISearchBarBackground")!) {
let imageView = view as! UIImageView
imageView.removeFromSuperview()
}
}
}
==================== Update Swift4 ====================
for subView in searchBar.subviews {
for view in subView.subviews {
if view.isKind(of: NSClassFromString("UINavigationButton")!) {
let cancelButton = view as! UIButton
cancelButton.setTitleColor(.white, for: .normal)
cancelButton.setTitle("取消", for: .normal)
}
if view.isKind(of: NSClassFromString("UISearchBarBackground")!) {
let imageView = view as! UIImageView
imageView.removeFromSuperview()
}
}
}
Alternate version as an extension
extension UISearchBar {
func removeBackgroundImageView(){
if let view:UIView = self.subviews.first {
for curr in view.subviews {
guard let searchBarBackgroundClass = NSClassFromString("UISearchBarBackground") else {
return
}
if curr.isKind(of:searchBarBackgroundClass){
if let imageView = curr as? UIImageView{
imageView.removeFromSuperview()
break
}
}
}
}
}
}
In my case it helped:
searchView.backgroundImage = UIImage()
searchView.searchTextField.backgroundColor = .white
The current answers will cause runtime errors if run within iOS 13:
Terminating app due to uncaught exception 'NSGenericException', reason:
'Missing or detached view for search bar layout. The application must not remove
<UISearchBarBackground: 0x102d05050; frame = (0 0; 414 56); alpha = 0; hidden = YES;
userInteractionEnabled = NO; layer = <CALayer: 0x280287420>> from the hierarchy.'
If the code must be run by devices between iOS 9 and iOS 13, then the below is a possible solution.
First, create an extension that allows for the recursive finding of a subview based on a class name:
extension UIView {
/// Find the first subview of the specified class.
/// - Parameter className: The class name to search for.
/// - Parameter usingRecursion: True if the search should continue through the subview tree until a match is found; false otherwise
/// - Returns: The first child UIView of the specified class
func findSubview(withClassName className: String, usingRecursion: Bool) -> UIView? {
// If we can convert the class name until a class, we look for a match in the subviews of our current view
if let reflectedClass = NSClassFromString(className) {
for subview in self.subviews {
if subview.isKind(of: reflectedClass) {
return subview
}
}
}
// If recursion was specified, we'll continue into all subviews until a view is found
if usingRecursion {
for subview in self.subviews {
if let tempView = subview.findSubview(withClassName: className, usingRecursion: usingRecursion) {
return tempView
}
}
}
// If we haven't returned yet, there was no match
return nil
}
}
Then, instead of removing the subview, make it fully transparent. The backgroundColorView view is the color that shows up directly underneath the text, but adjusting it is not a necessary part of the solution.
// On iOS 9, there is still an image behind the search bar. We want to remove it.
if let backgroundView = searchBar.findSubview(withClassName: "UISearchBarBackground", usingRecursion: true) {
backgroundView.alpha = 0
}
// The color on iOS 9 is white. This mimics the newer appearance of the post-iOS 9
// search controllers
if let backgroundColorView = searchBar.findSubview(withClassName: "_UISearchBarSearchFieldBackgroundView", usingRecursion: true) as? UIImageView {
backgroundColorView.backgroundColor = UIColor.lightGray
backgroundColorView.layer.cornerRadius = 8
backgroundColorView.alpha = 0.3
backgroundColorView.image = nil
}
I'm working on an Accessibility project where I have a segmentedController in the NavigationBar. Almost everything is working fine until the focus comes at the middle (2/3) SegmentedController. It won't speak the the accessibilityLabel..
See my code.
I'm using NSNotifications to let the 'UIAccessibilityPostNotification' know when to focus:
func chatLijst() {
let subViews = customSC.subviews
let lijstView = subViews.last as UIView
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, lijstView)
}
func berichtenLijst() {
let subViews = customSC.subviews
let messageView = subViews[1] as UIView
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, messageView)
}
func contactenLijst() {
let subViews = customSC.subviews
let contactenView = subViews.first as UIView
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, contactenView)
}
func setupSegmentedController(){
let lijst:NSString = "Lijst"
lijst.isAccessibilityElement = false
lijst.accessibilityLabel = "Lijst met gesprekken"
let bericht:NSString = "Bericht"
bericht.isAccessibilityElement = false
bericht.accessibilityLabel = "Bericht schrijven"
let contacten:NSString = "Contacten"
contacten.isAccessibilityElement = false
contacten.accessibilityLabel = "Contacten opzoeken"
let midden:CGFloat = (self.view.frame.size.width - 233) / 2
customSC.frame = CGRectMake(midden, 7, 233, 30)
customSC.insertSegmentWithTitle(lijst, atIndex: 0, animated: true)
customSC.insertSegmentWithTitle(bericht, atIndex: 1, animated: true)
customSC.insertSegmentWithTitle(contacten, atIndex: 2, animated: true)
customSC.selectedSegmentIndex = 0
customSC.tintColor = UIColor.yellowColor()
customSC.isAccessibilityElement = true
self.navigationController?.navigationBar.addSubview(customSC)
}
Fix
Strange enough I had to restructure the subViews array in the setup func and replace UIAccessibilityPostNotification object with the new segmentsView array.
func chatLijst() {
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, segmentsViews[0])
}
// Restructure subviews....
segmentsViews = [customSC.subviews[2], customSC.subviews[1], customSC.subviews[0]]
I'm using NSNotifications to let the 'UIAccessibilityPostNotification' know when to focus
Don't. That's a poor way to build a custom accessible control, and more importantly it can be confusing to the user. The screen changed notification doesn't just change focus, it also plays a specific sound that indicates to the user that the contents of the screen has changed.
Instead, I would recommend that you either make the subviews that you want appear as accessibility elements be accessibility elements with their own labels and traits and then rely on the OS to focus and activate them, or that you implement the UIAccessibilityContainer protocol in your custom control and then rely on the OS to focus and activate them.
I have a view with a Login button. When the button is clicked, I add a view with fields for login. When this happens, I need to dim the parent view. How I do that?
UIViews have a property named mask.
mask will always be on top of the UIView who owns it.
So, your approach should be something like this:
(This is for Swift, but it's easily converted to Obj-c)
self.view.mask = UIView(frame: self.frame)
self.view.mask?.backgroundColor = UIColor.black.withAlphaComponent(0.5)
//Do your work here, block UI, anything you need to, and then...
self.view.mask = nil
Update
Removed Swift 2 refernce as it's not relevant anymore. Just as a curiosity, then the property was called maskView
Add a UIView over the parent view that is initially transparent with a background color of black. When you need to dim it, change the view's alpha to 0.5. This will be 50% transparent.
I would go for a view with white background:
whiteView=[[UIView alloc]initWithFrame:viewToDim.frame];
[whiteView setBackgroundColor:[UIColor whiteColor]];
[whiteView setAlpha:0.5f];
[self.view insertSubview:whiteView aboveSubview:viewToDim];
class UIDecorator: NSObject {
static let sharedInstance = UIDecorator()
private let dimView = UIView()
private let loadingView = MOOverWatchLoadingView(frame: CGRectMake(0, 0, 100, 100),
autoStartAnimation: true)
func showLoadingView() {
if let currentPage = UIApplication.topViewController(){
dimView.frame = currentPage.view.frame
dimView.backgroundColor = UIColor.blackColor()
dimView.alpha = 0.5
currentPage.view.addSubview(dimView)
currentPage.view.userInteractionEnabled = false
loadingView.center = currentPage.view.center
loadingView.backgroundColor = UIColor.clearColor()
currentPage.view.addSubview(loadingView)
}
}
func dismissLocadingView() {
if let currentPage = UIApplication.topViewController(){
currentPage.view.userInteractionEnabled = true
dimView.removeFromSuperview()
loadingView.removeFromSuperview()
}
}
}