I'm developing an iOS app (the original code is not mine). The code below works fine in iOS 9/10 but on iOS 11 it enters an infinite loop of calling
addSubview.
import UIKit
class BlurView: UIVisualEffectView {
var isRounded: Bool
init(style: UIBlurEffectStyle, isRounded: Bool = true) {
self.isRounded = isRounded
super.init(effect: UIBlurEffect(style: style))
setupInterface()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func addSubview(_ view: UIView) {
guard view !== contentView else {
super.addSubview(view)
return
}
contentView.addSubview(view)
}
}
If I substitute the effect parameter in the line super.init(effect: UIBlurEffect(style: style)) for nil it works fine (of course, without the blur).
Additional information: inside the addSubView I can't access the contentView property, which seems to be the one that creates the stack overflow problem. On the init it looks like it's internally used, and that's where it breaks.
The questions are:
Why does this work on iOS 9/10 and not iOS 11?
How can I make this work on iOS 11?
Related
I've created a subclass to manage my Theme but is not showing neither on device or simulator.
Here my Header.swift:
import Foundation
import UIKit
class Header: UILabel {
override var textColor: UIColor! {
// White Color
get { return ThemeManager.currentTheme.palette.primary }
set {}
}
override var font: UIFont! {
get { return ThemeManager.currentTheme.textStyle.headerText }
set {}
}
}
Here the implementation: (inside the viewcontroller)
var titleLabel: Header = Header()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// Background Image over the view
setupBackground()
setupStartButton()
setupTitleLabel()
print(titleLabel.frame)
}
// MARK: - Header
private func setupTitleLabel() {
titleLabel.text = "0.0m"
// titleLabel.font = ThemeManager.currentTheme.textStyle.headerText
// titleLabel.textColor = ThemeManager.currentTheme.palette.primary
view.addSubview(titleLabel)
view.bringSubviewToFront(titleLabel)
setupTitleLabelAutolayout()
}
private func setupTitleLabelAutolayout() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
But if I use UILabel instead of Header it works perfectly as expected.
I've also tried to implement init?(coder aDecoder: NSCoder) and init(frame: CGRect) but nothing changed.
If I set a frame on init then shows the text, but not styled and ignoring my constraints.
Surely I'm missing something, but what?
To avoid usefulness answers, here some infos:
The UILabel textColor is white
The background is black and has an image over it.
I've tried to remove the image and all the stuff around except for the label and nothing changed.
That's a poor reason to use subclassing. It doesn't allow you to mix-and-match when appropriate.
Better would be to make an extension:
extension UILabel {
func withHeaderStyle() -> UILabel {
self.textColor = ThemeManager.currentTheme.palette.primary
self.font = ThemeManager.currentTheme.textStyle.headerText
return self
}
}
Then at point of use:
var titleLabel = UILabel().withHeaderStyle()
You can make several of these "withXStyle" methods and at the point of use you can chain them together. That's something you can't do with inheritance.
In general you should only use inherence when you want to change behavior. It's ill suited for changing data.
I've fixed that by editing the Header to this:
import Foundation
import UIKit
class Header: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
setupStyle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupStyle()
}
private func setupStyle() {
self.textColor = ThemeManager.currentTheme.palette.primary
self.font = ThemeManager.currentTheme.textStyle.headerText
}
}
Basically if I understood right, when I set the getter in the label it doesn't (if you think about, it's quite obvious) anything.
I still think that there are better solutions, but this works fine for me so, I'll keep that.
Now you may ask: "Why did you overwritten the getter instead of doing this?"
It's the right question, and the right answer is that I read it in a swift article on medium, so I tought it was right.
PS: I've also tried with didSet but it obviously loop through it self and crash.
I want to be able to add a background image to my ios app that applies for all views and not having to add this image to every single view. Is it possible to add an image to UIWindow or something?
Now, when I do a transition, the background of the new view also "transitions" over my previous view even though its the same background. I want to be able to only se the content transition, and not the background, which is the same.
You should be able to achieve this by:
subclassing UIWindow and overriding drawRect to draw your image; details about subclassing UIWindow here
and then by using UIAppearance to control the background colour for the rest of the views in your app (you should set it to clearColor); details about this here, though I think you'll need to subclass the views used by your controllers in order to cope with the note in the accepted answer
A basic implementation could look like this:
class WallpaperWindow: UIWindow {
var wallpaper: UIImage? = UIImage(named: "wallpaper") {
didSet {
// refresh if the image changed
setNeedsDisplay()
}
}
init() {
super.init(frame: UIScreen.mainScreen().bounds)
//clear the background color of all table views, so we can see the background
UITableView.appearance().backgroundColor = UIColor.clearColor()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
// draw the wallper if set, otherwise default behaviour
if let wallpaper = wallpaper {
wallpaper.drawInRect(self.bounds);
} else {
super.drawRect(rect)
}
}
}
And in AppDelegate:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? = WallpaperWindow()
the problem I am having is that I have reusable views / controls that contain text fields. These are xib files with a custom UI view class, such as the following:
import UIKit
#IBDesignable
public class CustomControl: UIControl {
#IBOutlet public weak var textField: UITextField?
public var contentView: UIView?
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViewFromNib()
}
override public init(frame: CGRect) {
super.init(frame: frame)
setupViewFromNib()
}
override public func awakeFromNib() {
super.awakeFromNib()
setupViewFromNib()
}
override public func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setupViewFromNib()
contentView?.prepareForInterfaceBuilder()
}
func setupViewFromNib() {
guard let view = loadViewFromNib() else { return }
guard let textField = self.textField else { return }
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let selfType = type(of: self)
let nibName = String(describing: selfType)
return Bundle(for: selfType)
.loadNibNamed(nibName, owner: self, options: nil)?
.first as? UIView
}
}
This custom view is being loaded into the Storyboards where they are to be used using the Storyboard Interface Builder.
The problem is that XCTest does not seem to model the descendants of these views, so when I am trying to write a test that involves typing text into the text field that is part of the custom view, the test bombs out with the error:
Neither element nor any descendant has keyboard focus.
Currently a work around appears to be to tap the keys on the keyboard instead of using the typeText method. However this is much slower (long pauses between key presses) and much more cumbersome test code wise.
The desired code:
let app = XCUIApplication()
let view = app.otherElements["customView"]
let textField = view.textFields["textField"]
textField.tap()
textField.typeText("12345")
Using test recording we get something like:
let app = XCUIApplication()
let view = app.otherElements["customView"]
view.tap()
app.typeText("12345")
But running this test causes the aforementioned error.
The edited / working test becomes:
let app = XCUIApplication()
let view = app.otherElements["customView"]
// view appears as a leaf with no descendants
view.tap()
app.keys["1"].tap()
app.keys["2"].tap()
app.keys["3"].tap()
app.keys["4"].tap()
app.keys["5"].tap()
I’m also not convinced this workaround will remain feasible if the custom view were to contain multiple controls, say perhaps for a date of birth control where I want more granular control over which field within the custom control I am using.
Is there a solution that allows me to access the fields within a custom view and potentially use the typeText method?
The problem has been solved. As advised by Titouan de Bailleul, the problem was that accessibility for the custom view had been enabled effectively hiding its descendant text fields.
Added sample project to Github:
https://github.com/stuartwakefield/XibXCTestIssueSample
Thanks Titouan.
I have a subclass of UITextView, which needs to have a specific default appearance. So far, I've been able to achieve this by overriding the initialize() class function, which has been deprecated in Swift 3.1.
public class CustomTextView : UITextView {
override public class func initialize() {
self.appearance().backgroundColor = .green
}
}
Is there a way to achieve the same thing pure Swift?
I'm working around the loss of the class method initialize by using a construct like this:
class CustomTextView : UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame:frame, textContainer: textContainer)
CustomTextView.doInitialize
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
CustomTextView.doInitialize
}
static let doInitialize : Void = {
CustomTextView.appearance().backgroundColor = .green
}()
}
This construct has the advantage that doInitialize will be initialized only once, so the code connected with it will run only once (in this case we'll configure the appearance proxy only once); and it is early enough to affect even the first instance created (that is, every CustomTextView you ever make will in fact be green).
I want to be able to add a background image to my ios app that applies for all views and not having to add this image to every single view. Is it possible to add an image to UIWindow or something?
Now, when I do a transition, the background of the new view also "transitions" over my previous view even though its the same background. I want to be able to only se the content transition, and not the background, which is the same.
You should be able to achieve this by:
subclassing UIWindow and overriding drawRect to draw your image; details about subclassing UIWindow here
and then by using UIAppearance to control the background colour for the rest of the views in your app (you should set it to clearColor); details about this here, though I think you'll need to subclass the views used by your controllers in order to cope with the note in the accepted answer
A basic implementation could look like this:
class WallpaperWindow: UIWindow {
var wallpaper: UIImage? = UIImage(named: "wallpaper") {
didSet {
// refresh if the image changed
setNeedsDisplay()
}
}
init() {
super.init(frame: UIScreen.mainScreen().bounds)
//clear the background color of all table views, so we can see the background
UITableView.appearance().backgroundColor = UIColor.clearColor()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
// draw the wallper if set, otherwise default behaviour
if let wallpaper = wallpaper {
wallpaper.drawInRect(self.bounds);
} else {
super.drawRect(rect)
}
}
}
And in AppDelegate:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? = WallpaperWindow()