Apple's tutorial describes the difference between init(frame:) and init?(coder:) as
You typically create a view in one of two ways: by programatically
initializing the view, or by allowing the view to be loaded by the
storyboard. There’s a corresponding initializer for each approach:
init(frame:) for programatically initializing the view and
init?(coder:) for loading the view from the storyboard. You will need
to implement both of these methods in your custom control. While
designing the app, Interface Builder programatically instantiates the
view when you add it to the canvas. At runtime, your app loads the
view from the storyboard.
I feel so confused by the description "programtically initializing" and "loaded by the storyboard". Say I have a subclass of UIView called MyView, does "programtically initialization" mean I write code to add an instance of MyView to somewhere like:
override func viewDidLoad() {
super.viewDidLoad()
let myView = MyView() // init(frame:) get invoked here??
}
while init?(coder:) get called when in Main.storyboard I drag a UIView from object library and then in the identity inspector I set its class to MyView?
Besides, in my xcode project, these two methods end up with different layout for simulator and Main.storyboard with the same code:
import UIKit
#IBDesignable
class RecordView: UIView {
#IBInspectable
var borderColor: UIColor = UIColor.clear {
didSet {
self.layer.borderColor = borderColor.cgColor
}
}
#IBInspectable
var borderWidth: CGFloat = 20 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable
var cornerRadius: CGFloat = 100 {
didSet {
layer.cornerRadius = cornerRadius
}
}
private var fillView = UIView()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupFillView()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupFillView()
}
private func setupFillView() {
let radius = (self.cornerRadius - self.borderWidth) * 0.95
fillView.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: radius * 2, height: radius * 2))
fillView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
fillView.layer.cornerRadius = radius
fillView.backgroundColor = UIColor.red
self.addSubview(fillView)
}
override func layoutSubviews() {
super.layoutSubviews()
}
func didClick() {
UIView.animate(withDuration: 1.0, animations: {
self.fillView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
}) { (true) in
print()
}
}
}
Why do they behave differently? (I drag a UIView from object library and set its class to RecordView)
I feel so confused by the description "programtically initializing" and "loaded by the storyboard".
Object-based programming is about classes and instances. You need to make an instance of a class. With Xcode, there are two broadly different ways to get an instance of a class:
your code creates the instance
you load a nib (such a view controller's view in the storyboard) and the nib-loading process creates the instance and hands it to you
The initializer that is called in those two circumstances is different. If your code creates a UIView instance, the designated initializer which you must call is init(frame:). But if the nib creates the view, the designated initializer that the nib-loading process calls is init(coder:).
Therefore, if you have a UIView subclass and you want to override the initializer, you have to think about which initializer will be called (based on how the view instance will be created).
First your delineation between init?(coder:) and init(frame:) is basically correct. The former is used when instantiating a storyboard scene when you actually run the app, but the latter is used when you programmatically instantiate it with either let foo = RecordView() or let bar = RecordView(frame: ...). Also, init(frame:) is used when previewing #IBDesignable views in IB.
Second, regarding your problem, I'd suggest you remove the setting of the center of fillView (as well as the corner radius stuff) from setupFillView. The problem is that when init is called, you generally don't know what bounds will eventually be. You should set the center in layoutSubviews, which is called every time the view changes size.
class RecordView: UIView { // this is the black circle with a white border
private var fillView = UIView() // this is the inner red circle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupFillView()
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
setupFillView()
}
private func setupFillView() {
fillView.backgroundColor = .red
self.addSubview(fillView)
}
override func layoutSubviews() {
super.layoutSubviews()
let radius = (cornerRadius - borderWidth) * 0.95 // these are not defined in this snippet, but I simply assume you omitted them for the sake of brevity?
fillView.frame = CGRect(origin: .zero, size: CGSize(width: radius * 2, height: radius * 2))
fillView.layer.cornerRadius = radius
fillView.center = CGPoint(x: bounds.midX, y: bounds.midY)
}
}
Related
I'm having trouble animating a layer on one of my views. I have googled the issue, but only find answers using CATransaction which assumes that I know the fromValue and toValue of its bounds. I have a view in a tableHeader that resizes itself when clicked. This view is an ordinary UIView and animates just as expected in an UIView.animate()-block. This view has a CAGradientLayer as a sublayer, to give it a gradient backgroundcolor. When the view animates its height, the layer does not animate with it. The layer changes its bounds immediately when the animation starts.
To make sure the layer gets the right size overall (during init/loading/screen rotation etc.) I have been told to do this:
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = backgroundView.bounds
}
It gets the right size every time, but it never animates to it.
To do my view-animation, I do this:
self.someLabelHeightConstraint.constant = someHeight
UIView.animate(withDuration: 0.3, animations: { [weak self] in
self?.layoutIfNeeded()
})
which works perfectly, but I assume layoutIfNeeded() calls layoutSubviews at some point, which I assume will ruin any CALayer-animations I add into the block.
As you can see, I only change the constant of a constraint set on a view inside my view, so I actually don't know what the size of the actual header-view will be when the animation is completed. I could probably do some math to figure out what it'll be, but that seems unnecessary..
Are there no better ways to do this?
There are kludgy ways to update a sublayer's frame during an animation, but the most elegant solution is to let UIKit take care of this for you. Create a subclass UIView whose layerClass is a CAGradientLayer. When the view is created, the CAGradientLayer will be created for you. And when you animate the view's frame, the gradient layer is animated gracefully for you.
#IBDesignable
public class GradientView: UIView {
#IBInspectable public var startColor: UIColor = .white { didSet { updateColors() } }
#IBInspectable public var endColor: UIColor = .black { didSet { updateColors() } }
override open class var layerClass: AnyClass { return CAGradientLayer.self }
override public init(frame: CGRect = .zero) {
super.init(frame: frame)
config()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
config()
}
private func config() {
updateColors()
}
private func updateColors() {
let gradientLayer = layer as! CAGradientLayer
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
}
}
Note, I've made this #IBDesignable so you can put it in a framework target and then use it in IB, but that's up to you. That's not required. The main issue is the overriding of layerClass so that UIKit takes care of the animation of the layer as it animates the view.
You need to remove the actions of the CALayer
Add this code
gradientLayer.actions = ["position": NSNull(),"frame":NSNull(),"bounds":NSNull()]
I want to understand why we use super.init all the time.
For example,
class HeaderStyle1: UIView {
var subview: UIView! //NİB
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit(){
subview = Bundle.main.loadNibNamed("HeaderStyle1", owner: self, options: nil)?.first as! UIView
subview.frame = bounds
subview.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(subview)
}
First: When I instantiate
header = HeaderStyle1(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 80))
super.init(frame: frame) instantiates UIView. Why we need this?
class ViewController: UIViewController {
var header: HeaderStyle1!
override func viewDidLoad() {
super.viewDidLoad()
createView() }
func createView() {
//Create Header
header = HeaderStyle1(frame: CGRect(x: 0, y: 0, width: self.view.fr
self.view.addSubview(header)
}
Second: I add header to UIViewController view as subview. Here header is an instance of HeaderStyle1. So I actually add a class to UIViewController view. How does UIViewController view show instance as view? Is it because of super.init or something?
Thank you
self.view.addSubview(header)
First , as every class is a subclass of another You call super.init is done to insure that if you call some Instance Variable / Method of the super class, you'll be able to do so as the super class is successfully initiated
suppose that i sublassed NSArray and in some point in app i want to call
NSArray*er = [myArr mutableCopy];
the copy process will fail if [super init]; failed when i subclassed NSArray
as mutableCopy is a method in NSObject that i ignored it's super init
Second , you actually adding an instance of a UIView (HeaderStyle1) to the current view controller's view (which also an instance of UIView) not class to class , this is a hierarchy made by Apple so developers can add different views to their apps to satisfy certain layout needs and modularize their Design so on for all UIKit components (UILabel,UITextfeild.....) that are pre-designed in IOS frameworks to display common components that make sense in small Mobile devices
I have a UICollectionViewController embedded inside a UINavigationController which in turn embedded inside a UITabBarController.
I want to add a UIView to the UICollectionViewController just above the tab bar (shown by red rectangle).
I have the UIView created separately as a nib file.
import UIKit
class BottomView: UIView {
#IBOutlet var view: UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
fileprivate func commonInit() {
Bundle.main.loadNibNamed("BottomView", owner: self, options: nil)
view.frame = self.frame
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(view)
}
}
And I initialize and add it in the UICollectionViewController like so.
class CollectionViewController: UICollectionViewController {
fileprivate var bottomView: BottomView!
override func viewDidLoad() {
super.viewDidLoad()
let yPos = view.bounds.height - (tabBarController!.tabBar.frame.size.height + 44)
bottomView = BottomView(frame: CGRect(x: 0, y: yPos, width: view.bounds.width, height: 44))
collectionView?.addSubview(bottomView)
}
// ...
}
I figured if I substract the combined height of the bottom view plus the tab bar from the entire view's height, I should be able to get the correct y position value. But it's not happening. The view is getting added but way off screen.
How do I calculate the correct y position without hardcoding it?
Example demo project
I would suggest adding the BottomView to the UICollectionViewController's view rather than to the collection view itself. This is part of the problem you're having.
You're also trying to set the frame of the BottomView in the viewDidLoad() method using values from view.bounds. The CGRect will return (0.0, 0.0, 0.0, 0.0) at this point because the layout has yet to take place, which is most likely why your positioning is off. Try moving your layout logic to the viewWillLayoutSubviews() method and see if that helps.
A better approach would be by setting auto layout constrains rather than defining a frame manually, this will take a lot of the leg work out for you.
Here's a quick example:
self.bottomView.translatesAutoresizingMaskIntoConstraints = false
self.view.insertSubview(self.bottomView, at: 0)
self.bottomView.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor).isActive = true
self.bottomView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.bottomView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
self.bottomView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
You can apply autolayout logic in your viewDidLoad() and it should work correctly.
You can find some more information on setting autolayout constraints programatically here:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html
Sounds what you want to achieve is exactly the footer view for the UICollectionView.
A footerView is like a view that will stick to the bottom of the collectionView and wont move with the cells.
This will help you add a footer View: https://stackoverflow.com/a/26893334/3165112
Hope that helps!
When the app run in Simulator it works but in Storyboard can’t see the preview, why?
Swift code:
Simulator and Storyboard:
Can’t see the custom view background color and when drag UIButton object into custom view doesn’t see the real position(x, y) and its background color.
In Android Studio when you add an object (custom view) in layout.xml file you can see the preview automatically, is it possible to do the same thing in Xcode?
TL;DR; Call in prepareForInterfaceBuilder
// Call The Custom Setup Here
override func prepareForInterfaceBuilder() {
setupView()
}
Calling in layoutSubviews also works, but is called multiples times in runtime, prepareForInterfaceBuilder is called only for Designables Changes, and only with this purpose.
Long Code:
#IBDesignable
class CustomView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
override func prepareForInterfaceBuilder() {
setupView()
}
func InitChildPosition() {
var i = 1
for _view in self.subviews {
if _view is UIButton {
_view.center.x = (_view.bounds.width / 2)
_view.center.y = (_view.bounds.height / 2)
}
if _view is UIButton && i == 2 {
_view.center.x = self.bounds.width - (_view.bounds.width / 2)
_view.center.y = self.bounds.height - (_view.bounds.height / 2)
_view.backgroundColor = UIColor.darkGray
}
i += 1
}
}
func setupView() {
self.backgroundColor = UIColor.black
InitChildPosition()
}
}
Call your CustomSetup() from init(frame:)
EDIT
Interface Builder uses init(frame:) to create and position views in storyboards.
You are modifying the subviews of CustomView in CustomSetup() which is called from the initializer. Your view does not have any button subviews in that case, because subviews can only be added after the view has been initialized.
You need to defer calling CustomSetup() to a later point, awakeFromNib() should work in your case.
Better yet, remove CustomSetup() completely and just style your UIButtons in the Interface Builder. You will get the same results without all the complexity.
Background
In order to make a text view that scrolls horizontally for vertical Mongolian script, I made a custom UIView subclass. The class takes a UITextView, puts it in a UIView, rotates and flips that view, and then puts that view in a parent UIView.
The purpose for the rotation and flipping is so that the text will be vertical and so that line wrapping will work right. The purpose of sticking everything in a parent UIView is so that Auto layout will work in a storyboard. (See more details here.)
Code
I got a working solution. The full code on github is here, but I created a new project and stripped out all the unnecessary code that I could in order to isolate the problem. The following code still performs the basic function described above but also still has the slow loading problem described below.
import UIKit
#IBDesignable class UIMongolTextView: UIView {
private var view = UITextView()
private var oldWidth: CGFloat = 0
private var oldHeight: CGFloat = 0
#IBInspectable var text: String {
get {
return view.text
}
set {
view.text = newValue
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect){
super.init(frame: frame)
}
override func sizeThatFits(size: CGSize) -> CGSize {
// swap the length and width coming in and going out
let fitSize = view.sizeThatFits(CGSize(width: size.height, height: size.width))
return CGSize(width: fitSize.height, height: fitSize.width)
}
override func layoutSubviews() {
super.layoutSubviews()
// layoutSubviews gets called multiple times, only need it once
if self.frame.height == oldHeight && self.frame.width == oldWidth {
return
} else {
oldWidth = self.frame.width
oldHeight = self.frame.height
}
// Remove the old rotation view
if self.subviews.count > 0 {
self.subviews[0].removeFromSuperview()
}
// setup rotationView container
let rotationView = UIView()
rotationView.frame = CGRect(origin: CGPointZero, size: CGSize(width: self.bounds.height, height: self.bounds.width))
rotationView.userInteractionEnabled = true
self.addSubview(rotationView)
// transform rotationView (so that it covers the same frame as self)
rotationView.transform = translateRotateFlip()
// add view
view.frame = rotationView.bounds
rotationView.addSubview(view)
}
func translateRotateFlip() -> CGAffineTransform {
var transform = CGAffineTransformIdentity
// translate to new center
transform = CGAffineTransformTranslate(transform, (self.bounds.width / 2)-(self.bounds.height / 2), (self.bounds.height / 2)-(self.bounds.width / 2))
// rotate counterclockwise around center
transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
// flip vertically
transform = CGAffineTransformScale(transform, -1, 1)
return transform
}
}
Problem
I noticed that the custom view loads very slowly. I'm new to Xcode Instruments so I watched the helpful videos Debugging Memory Issues with Xcode and Profiler and Time Profiler.
After that I tried finding the issue in my own project. It seems like no matter whether I use the Time Profiler or Leaks or Allocations tools, they all show that my class init method is doing too much work. (But I kind of knew that already from the slow load time before.) Here is a screen shot from the Allocations tool:
I didn't expand all of the call tree because it wouldn't have fit. Why are so many object being created? When I made a three layer custom view I knew that it wasn't ideal, but the number of layers that appears to be happening from the call tree is ridiculous. What am I doing wrong?
You shouldn't add or delete any subview inside layoutSubviews, as doing so triggers a call to layoutSubviews again.
Create your subview when you create your view, and then only adjust its position in layoutSubviews rather than deleting and re-adding it.