As I understand it, there is a new method to configure UITableView cells for iOS 14. I am using this method, but when I implemented it, there was an error.
Exception NSException * "The content view returned from the content configuration must have translatesAutoresizingMaskIntoConstraints enabled"
However, when I set translatesAutoresizingMaskIntoConstraints to true, I could no longer layout the view for the cell. That is, even though I was setting the view's width, nothing was displayed.
Below is the method I am using (without my code that configures the views). My question is whether there is a way to avoid the error while still being able to layout the view.
#available(iOS 14, *)
extension GoalCell {
struct Configuration: UIContentConfiguration {
func makeContentView() -> UIView & UIContentView {
return MyContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> GoalCell.Configuration {
return self
}
}
class MyContentView: UIView, UIContentView {
var configuration: UIContentConfiguration {
didSet {
self.configure()
}
}
}()
init(configuration: UIContentConfiguration) {
self.configuration = configuration
super.init(frame: .zero)
self.configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
guard let config = self.configuration as? Configuration else { return }
self.translatesAutoresizingMaskIntoConstraints = true
}
}
Related
I am using a Representable ViewController for a custom NSTextField in SwiftUI for MacOS. I am applying a customized, increased font size for that TextField. That works fine, I had a question regarding that topic in my last question here.
I am setting the Font of the TextField in my viewDidAppear() method now, which works fine. I can see a bigger font. The problem is now, when my view is not being in focus, the text size is shrinking back to normal. When I go back and activate the window again, I see the small font size. When I type again, it gets refreshed and I see the correct font size.
That is my code I am using:
class AddTextFieldController: NSViewController {
#Binding var text: String
let textField = NSTextField()
var isFirstResponder : Bool = true
init(text: Binding<String>, isFirstResponder : Bool = true) {
self._text = text
textField.font = NSFont.userFont(ofSize: 16.5)
super.init(nibName: nil, bundle: nil)
NSLog("init")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
textField.delegate = self
//textField.font = NSFont.userFont(ofSize: 16.5)
self.view = textField
}
override func viewDidAppear() {
self.view.window?.makeFirstResponder(self.view)
textField.font = NSFont.userFont(ofSize: 16.5)
}
}
extension AddTextFieldController: NSTextFieldDelegate {
func controlTextDidChange(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
self.text = textField.stringValue
}
textField.font = NSFont.userFont(ofSize: 16.5)
}
}
And this is the representable:
struct AddTextFieldRepresentable: NSViewControllerRepresentable {
#Binding var text: String
func makeNSViewController(
context: NSViewControllerRepresentableContext<AddTextFieldRepresentable>
) -> AddTextFieldController {
return AddTextFieldController(text: $text)
}
func updateNSViewController(
_ nsViewController: AddTextFieldController,
context: NSViewControllerRepresentableContext<AddTextFieldRepresentable>
) {
}
}
Here is a demo:
I thought about using Notification when the view is coming back, however not sure
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: NSApplication.willBecomeActiveNotification, object: nil)
Here is a solution. Tested with Xcode 11.4 / macOS 10.15.4
class AddTextFieldController: NSViewController {
#Binding var text: String
let textField = MyTextField() // << overridden field
and required custom classes (both!!) for cell & control (in all other places just remove user font usage as not needed any more):
class MyTextFieldCell: NSTextFieldCell {
override var font: NSFont? {
get {
return super.font
}
set {
// system tries to reset font to default several
// times (here!), so don't allow that
super.font = NSFont.userFont(ofSize: 16.5)
}
}
}
class MyTextField: NSTextField {
override class var cellClass: AnyClass? {
get { MyTextFieldCell.self }
set {}
}
override var font: NSFont? {
get {
return super.font
}
set {
// system tries to reset font to default several
// times (and here!!), so don't allow that
super.font = NSFont.userFont(ofSize: 16.5)
}
}
}
I was using NotificationCenter to observe interfaceOrientation change by UIApplication.willChangeStatusBarOrientationNotification. But UIApplication.willChangeStatusBarOrientationNotification is now marked deprecated. It is suggested to use viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator, but this is the method called from the ViewController. I have a subclass of UIView that needs this internally, and Im exposing this View as a framework. I would like to be able to still maintain this functionality without the need to be explicitly called from the ViewController. Is there another option to achieve this directly inside View? I tried observing UIWindowScene.interfaceOrientation with KVO, but this didn't work.
public extension Notification.Name {
static let willChangeStatusBarOrientationNotification = UIApplication.willChangeStatusBarOrientationNotification
}
...
#objc private func statusBarOrientationWillChange(notification: Notification) {
if let orientationRawValue = notification.userInfo?[UIApplication.statusBarOrientationUserInfoKey] as? Int, let orientation = AVCaptureVideoOrientation(rawValue: orientationRawValue) {
configureVideoOrientation(interfaceOrientation: orientation)
updateSceneInformation()
}
}
...
private func initialize() {
NotificationCenter.default.addObserver(self, selector: #selector(statusBarOrientationWillChange(notification:)), name: .willChangeStatusBarOrientationNotification, object: nil)
}
Try this. It seems to only be available as a post-orientation-change notification and not a pre-orientation-change notification, but it should not produce a deprecation warning. Note that the device orientation is of the UIDeviceOrientation type, not the UIInterfaceOrientation type.
public class MyView: UIView {
public override init(frame: CGRect) {
super.init(frame: frame)
NotificationCenter.default.
addObserver(self,
selector: #selector(deviceOrientationDidChange),
name: UIDevice.orientationDidChangeNotification,
object: nil)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
#objc
private func deviceOrientationDidChange(_ notification: Notification) {
guard UIDevice.current.orientation.isValidInterfaceOrientation,
let orientation = AVCaptureVideoOrientation(rawValue: UIDevice.current.orientation.rawValue)
else { return }
configureVideoOrientation(interfaceOrientation: orientation)
updateSceneInformation()
}
}
I wanted to implement my own HUD for a UIViewCntroller and a UIView, so I did this:
protocol ViewHudProtocol {
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
Now I can easily adopt ViewHudProtocol on any UIView to call showLoadingView and hideLoadingView. The problem is I want to use the same protocol for UIViewController, so I did this:
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.view.showLoadingView() //Error: UIView has no member showLoadingView
}
func hideLoadingView() {
self.view.hideLoadingView() //Error: UIView has no member hideLoadingView
}
}
I agree to the error that UIView has not adopted the protocol yet. So I did this:
extension UIView: ViewHudProtocol {}
And it works. Is there a better way to do this? I mean it feels wrong to extend every view with ViewHudProtocol, where not all of them will use it. If I could do something like, "only adopt ViewHudProtocol implicitly for a UIView, if its UIViewController demands for it. Else you could adopt ViewHUDProtocol manually on any UIView when required."
I would solve this with the following approach, using associatedtype, defined only for needed views and/or controllers (tested in Xcode 11.2 / swift 5.1):
protocol ViewHudProtocol {
associatedtype Content : ViewHudProtocol
var content: Self.Content { get }
func showLoadingView()
func hideLoadingView()
}
extension ViewHudProtocol where Self: UIView {
var content: some ViewHudProtocol {
return self
}
func showLoadingView() { //Show HUD by adding a custom UIView to self.}
}
func hideLoadingView() {
}
}
extension ViewHudProtocol where Self: UIViewController {
func showLoadingView() {
self.content.showLoadingView() //NO Error
}
func hideLoadingView() {
self.content.hideLoadingView() //NO Error
}
}
//Usage
extension UITableView: ViewHudProtocol { // only for specific view
}
extension UITableViewController: ViewHudProtocol { // only for specific controller
var content: some ViewHudProtocol {
return self.tableView
}
}
The problem
So you want to constraint the conformance of a UIViewController to the protocol ViewHudProtocol only when the UIViewController.view property conforms to ViewHudProtocol.
I am afraid this is not possible.
Understanding the problem
Let's have a better look at your problem
You have 2 types (UIView and UIViewController) and you want to add to both the same functionalities
func showLoadingView()
func hideLoadingView()
What Mick West teaches us
This kind of scenario is somehow similar to what Mick West faced during the development of the Tony Hawks series Mick West and an elegant solution is described in its article Evolve your hierarchy.
Solution
We can apply that approach to your problem and here's the solution
struct HudViewComponent {
let view: UIView
private let hud: UIView
init(view: UIView) {
self.view = view
self.hud = UIView(frame: view.frame)
self.hud.isHidden = true
self.view.addSubview(hud)
}
func showLoadingView() {
self.hud.isHidden = false
}
func hideLoadingView() {
self.hud.isHidden = true
}
}
protocol HasHudViewComponent {
var hidViewComponent: HudViewComponent { get }
}
extension HasHudViewComponent {
func showLoadingView() {
hidViewComponent.showLoadingView()
}
func hideLoadingView() {
hidViewComponent.hideLoadingView()
}
}
That's it, now you can add the hud functionalities to any Type conforming to HasHudViewComponent.
class SomeView: UIView, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self) }()
}
or
class MyViewController: UIViewController, HasHudViewComponent {
lazy var hidViewComponent: HudViewComponent = { return HudViewComponent(view: self.view) }()
}
Considerations
As you can see the idea is to thinking in terms of components.
You build a component (HudViewComponent) with your hud functionalities. The component only asks for the minimum requirements: it needs a UIView.
Next you define the HasHudViewComponent which states that the current type has a HudViewComponent property.
Finally you can add your hud functionalities to any Type which has a view (UIView, UIViewController, ...) simply conforming your type to HasHudViewComponent.
Notes
You asked an interesting question and I know this does not answers 100% what you were looking for, but by a practical point of view it should provides you with a tool to achieve what you need.
I would have taken this approach:
Create a UIView Class,
setup the view
Declare a shared object.
A function to show the view
A function to remove the view. and then call it in view controllers as IndicatorView.shared.show() , IndicatorView.shared.hide()
import Foundation
import UIKit
import Lottie
class IndicatorView : UIView {
static let shared = IndicatorView()
var loadingAnimation : AnimationView = {
let lottieView = AnimationView()
lottieView.translatesAutoresizingMaskIntoConstraints = false
lottieView.layer.masksToBounds = true
return lottieView
}()
var loadingLabel : UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont(name: "SegoeUI", size: 12)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func show() {
setupLoadingView()
self.alpha = 0
UIView.animate(withDuration: 0.5, animations: {
self.isHidden = false
self.alpha = 1
}, completion: nil)
applyLottieAnimation()
}
public func hide() {
self.alpha = 1
UIView.animate(withDuration: 0.5, animations: {
self.alpha = 0
}, completion: { _ in
self.isHidden = true
self.removeFromSuperview()
}) }
private func setupLoadingView() {
let controller = UIApplication.shared.keyWindow!.rootViewController!
controller.view.addSubview(self)
//setup your views here
self.setNeedsLayout()
self.reloadInputViews()
}
}
For this particular scenario, a Decorator would work better, and result in a better design:
final class HUDDecorator {
private let view: UIView
init(_ view: UIView) {
self.view = view
}
func showLoadingView() {
// add the spinner
}
func hideLoadingView() {
// remove the spinner
}
}
Using the Decorator would then be as easy as declaring a property for it:
class MyViewController: UIViewController {
lazy var hudDecorator = HUDDecorator(view)
}
This will allow any controller to decide if it wants support for showing/hiding a loading view by simply exposing this property.
Protocols are too invasive for simple tasks like enhancing the looks on a UI component, and they have the disadvantage of forcing all views of a certain class to expose the protocol functionality, while the Decorator approach allows you to decide which view instances to receive the functionality.
We have a requirement in our project. We need to set accessibility identifier for all the components in approximately 40 view controllers. I was thinking how to achieve these basic work by getting each view controller name and iboutlet names in run time and generate ids by combining these values as accessibility id. For these, I need to get IBOutlet's names. How can I do that ? Or do you have any alternative idea for automating this process another way ?
Thanks.
You can try Sourcery
It able to parse all your source files and provide you information about IBOutlets of all controllers:
You interested in classes -> variables -> attributes
You can generate inline for all such variables didSet block in which you will setup proper accessibility identifier
you can check this link and you find a great solution to automate setting accessibilityIdentifier with the same name of the variable
https://medium.com/getpulse/https-medium-com-dinarajas-fixing-developer-inconveniences-ios-automation-e4832108051f
import UIKit
protocol AccessibilityIdentifierInjector {
func injectAccessibilityIdentifiers()
}
extension AccessibilityIdentifierInjector {
func injectAccessibilityIdentifiers() {
var mirror: Mirror? = Mirror(reflecting: self)
repeat {
if let mirror = mirror {
injectOn(mirror: mirror)
}
mirror = mirror?.superclassMirror
} while (mirror != nil)
}
private func injectOn(mirror: Mirror) {
for (name, value) in mirror.children {
if var value = value as? UIView {
UnsafeMutablePointer(&value)
.pointee
.accessibilityIdentifier = name
}
}
}
}
extension UIViewController: AccessibilityIdentifierInjector {}
extension UIView: AccessibilityIdentifierInjector {}
class BaseView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
injectAccessibilityIdentifiers()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
injectAccessibilityIdentifiers()
}
}
How to use it
class ViewController: BaseViewController {
let asd1 = BaseView()
let asd2 = BaseView()
let asd3 = BaseView()
let asd4 = BaseView()
let asd5 = BaseView()
let asd6 = BaseView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
[asd1, asd2, asd3, asd4, asd5, asd6].forEach {
print($0.accessibilityIdentifier)
}
}
}
I am lacking some basic understandings.
Today I wanted to subclass some UIView and I looked in to the UIButton definition, but I can not figure out how it works.
In the UIButton definition are properties like:
open var adjustsImageWhenHighlighted: Bool
open var adjustsImageWhenDisabled: Bool
When using an UIButton it does not matter when the values of the UIButton get set, it always gets configured the correct way same with tableView or any other UIKit classes.
I made a example:
class customView: UIView {
var shouldSetupConstraints = true
var addAdditionalSpacing = true
var elements: [String]? {
didSet{
if addAdditionalSpacing == false {
doSomething()
}
}
}
func doSomething() {
}
override init(frame: CGRect) {
super.init(frame: frame)
setUpLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func updateConstraints() {
if(shouldSetupConstraints) {
// AutoLayout constraints
shouldSetupConstraints = false
}
super.updateConstraints()
}
func setUpLayout() {
}
}
Using CustomView:
lazy var customV: CustomView = {
let v = CustomView()
v.addAdditionalSpacing = true
v.elements = ["One","Two"]
return v
}()
lazy var customV2: CustomView = {
let v = CustomView()
v.elements = ["One","Two"]
v.addAdditionalSpacing = true
return v
}()
So if I am using CustomView it makes a difference in which order I set it up, I understand why but I do not understand in which way I have to design my classes so I can set the values whenever I want, except with different if didSet configurations. Why do the properties in UIButton do not have any setters, how do the values get set in that case?
Any links to documentations are appreciated as well.
First of all, in a UIButton you can take control of the didSet property observer for existing properties like this:
override var adjustsImageWhenDisabled: Bool {
didSet {
doStuff()
}
}
Secondly, in your class scenario you might consider passing the parameters in the constructor:
init(shouldSetupConstraints: Bool = true, var addAdditionalSpacing = true)
This way the properties will be available for you whenever you need them.
Let me know if this helps.