I'm trying to build a UIView that lays out other views using their intrinsic width, left to right, and then wraps if necessary. UICollectionView seems overly complicated for what I need. I also tried using the FlexLayout wrapper for Yoga, and couldn't seem to get the right incantation. It seems like it should be much easier than this -- what's the simple solution?
I ended up writing my own UIView to solve the problem. I doubt if it's very robust, but it solves my immediate needs. Please, comment if you have an improvements to suggest.
import UIKit
class FlowLayout: UIView {
private var hgapValue: CGFloat = 0.0
private var vgapValue: CGFloat = 0.0
var hgap: CGFloat {
set(newValue) {
hgapValue = (newValue < 0) ? -newValue : newValue
}
get {
return hgapValue
}
}
var vgap: CGFloat {
set(newValue) {
vgapValue = (newValue < 0) ? -newValue : newValue
}
get {
return vgapValue
}
}
//initWithFrame to init view from code
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
override func didAddSubview(_ subview: UIView) {
refreshLayout()
}
//common func to init our view
private func setupView() {
// backgroundColor = .red
}
public func refreshLayout() {
var x: CGFloat = 0.0
var y: CGFloat = 0.0
for subview in subviews {
subview.frame.origin.x = x
subview.frame.origin.y = y
x += subview.frame.width + hgapValue
if x + subview.frame.width >= (subview.superview?.frame.width)! {
x = 0.0
y += subview.frame.height + vgapValue
}
}
setNeedsDisplay()
}
}
In interface builder, using UIStackView to achieve a simple alternative.
You can also turn on auto layout to make it naturally.
Related
I'm trying to animate a logo (UILabel) for my app, from the middle to the top. What I tried was to update the constraint but it doesn't seem to work. The problem is the animation i.e. logo, goes from the origin (0,0) and not the middle of the view to the top. The necessary code (the controller and the class it inherits):
import UIKit
import SnapKit
class EntryController: LatroController {
static let spacingFromTheTop: CGFloat = 150
var latroLabelCenterYConstraint: Constraint?
override init() {
super.init()
self.animateTitleLabel()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func initTitleLabel() {
self.latroLabel = UILabel()
self.latroLabel?.text = General.latro.rawValue
self.latroLabel?.textAlignment = .center
self.latroLabel?.font = UIFont (name: General.latroFont.rawValue, size: EntryController.fontSize)
self.latroLabel?.textColor = .white
self.latroLabel?.contentMode = .center
self.view.addSubview(self.latroLabel!)
self.latroLabel?.snp.makeConstraints({ (make) in
make.width.equalTo(EntryController.latroWidth)
make.height.equalTo(EntryController.latroHeight)
make.centerX.equalTo(self.view.center.x)
self.latroLabelCenterYConstraint = make.centerY.equalTo(self.view.center.y).constraint
})
}
func animateTitleLabel() {
UIView.animate(withDuration: 1.5) {
self.latroLabel?.snp.updateConstraints { (make) in
make.centerY.equalTo(200)
}
self.view.layoutIfNeeded()
}
}
}
import UIKit
import SnapKit
class LatroController: UIViewController {
static let latroWidth: CGFloat = 288
static let latroHeight: CGFloat = 98
static let btnWidth: CGFloat = 288
static let btnHeight: CGFloat = 70
static let txtFieldWidth: CGFloat = 288
static let txtFieldHeight: CGFloat = 50
static let fontSize: CGFloat = 70
static let bottomOffset: CGFloat = 100
static let buttonOffset: CGFloat = 20
static let logoOffset: CGFloat = 50
var latroLabel: UILabel?
var signUpBtn: UIButton?
var logInBtn: UIButton?
var titleLabelYConstraint: NSLayoutConstraint?
var usernameTxtField: UITextField?
init() {
super.init(nibName: nil, bundle: nil)
self.view.backgroundColor = UIColor(named: General.orange.rawValue)
self.initTitleLabel()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: false)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
func initTitleLabel() {
self.latroLabel = UILabel()
self.latroLabel?.text = General.latro.rawValue
self.latroLabel?.textAlignment = .center
self.latroLabel?.font = UIFont (name: General.latroFont.rawValue, size: EntryController.fontSize)
self.latroLabel?.textColor = .white
self.latroLabel?.contentMode = .center
self.view.addSubview(self.latroLabel!)
self.latroLabel?.snp.makeConstraints({ (make) in
make.width.equalTo(LatroController.latroWidth)
make.height.equalTo(LatroController.latroHeight)
let safeAreaLayoutHeight = self.view.safeAreaLayoutGuide.layoutFrame.height
print(safeAreaLayoutHeight)
make.top.equalTo(self.view).offset(150)
make.centerX.equalTo(self.view.center.x)
})
}
}
You cannot animate a view until it is in the interface and initial layout has been performed. Thus you are calling self.animateTitleLabel() way too soon (in init).
Call it in something like viewDidAppear. Of course then you must use a Bool flag property to make sure you don't call it every time viewDidAppear runs, only the first time.
(It might be necessary to call it in viewDidLayoutSubviews instead; you'll have to experiment.)
Okay, thought it would be tougher than I initially expected. The following was missing:
self.view.updateLayoutIfNeeded()
after setting constraints!
I have a custom UIView and I add it to my ViewController like this:
let myCustomView = Bundle.main.loadNibNamed("MyCustomView", owner: nil, options: nil) as! MyCustomView
myCustomView.layer.cornerRadius = 10
myCustomView.layer.masksToBounds = true
I round the corners of the view. But I am wondering, is there a way to move this logic of rounding the corners inside the MyCustomView class?
As you use IB, you may find it more convenient to make an extension of UIView
extension UIView {
#IBInspectable var borderColor: UIColor? {
set {
layer.borderColor = newValue?.cgColor
}
get {
if let color = layer.borderColor {
return UIColor(cgColor:color)
} else {
return nil
}
}
}
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
clipsToBounds = newValue > 0
}
get {
return layer.cornerRadius
}
}
}
Then you can set those values from Attributes inspector.
Yes - If you're loading a nib with a custom view, that nib is very likely referring to another class. If that's the case, you can move the logic inside the class itself.
That said, I really like Lawliet's suggestion of making a UIView extension with IBInspectable properties. The downside to that approach is that every single view now has these properties, which creates a certain overhead and potential for clashes.
You can do something like this in your UIView subclass:
class RoundedView: UIView {
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
}
}
Also if you want to pass a custom value instead of '10' in the cornerRadius property, you can try to implement a convenience init by looking here:
Override Init method of UIView in Swift
In the screen shot below please note that the DropDownButton (the selected view) is not being live rendered. Also, please note the "Designables Up To Date" in the Identity Inspector. Finally, please note the two break points in the assistant editor: if I execute Editor -> Debug Selected Views then both of these break points are hit.
Here's what It looks like when I run it:
Here's the code:
#IBDesignable
class DropDownButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
if image(for: .normal) == nil {
//setImage(#imageLiteral(resourceName: "DropDown"), for: .normal)
let bundle = Bundle(for: DropDownButton.self)
if let image = UIImage(named: "DropDown", in: bundle, compatibleWith: nil) {
setImage(image, for: .normal) // Editor -> Debug Selected Views reaches this statement
}
}
if title(for: .normal) == nil {
setTitle("DropDown", for: .normal) // Editor -> Debug Selected Views reaches this statement
}
addTarget(self, action: #selector(toggle), for: .touchUpInside)
}
override func layoutSubviews() {
super.layoutSubviews()
if var imageFrame = imageView?.frame, var labelFrame = titleLabel?.frame {
labelFrame.origin.x = contentEdgeInsets.left
imageFrame.origin.x = labelFrame.origin.x + labelFrame.width + 2
imageView?.frame = imageFrame
titleLabel?.frame = labelFrame
}
}
override func setTitle(_ title: String?, for state: UIControlState) {
super.setTitle(title, for: state)
sizeToFit()
}
public var collapsed: Bool {
return imageView?.transform == CGAffineTransform.identity
}
public var expanded: Bool {
return !collapsed
}
private func collapse() {
imageView?.transform = CGAffineTransform.identity
}
private func expand() {
imageView?.transform = CGAffineTransform(rotationAngle: .pi)
}
#objc private func toggle(_: UIButton) {
if collapsed { expand() } else { collapse() }
}
}
First Edit:
I added the prepareForInterfaceBuilder method as per #DonMag's answer. Doing so made an improvement but there is still something wrong: Interface builder seems confused about the frame. When I select the button only the title is selected, not the image (i.e. triangle). I added a border; it goes around both the title and the image. Here is a picture:
If I drag the button to a new position then everything moves, title and image.
Also, it surprised me that prepareForInterfaceBuilder made a difference. My understanding of this method it that it allows me to do interface builder only setup such as providing dummy data.
Add this:
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
initialize()
}
I'm having a problem with the graphOrigin property in my UIView subclass. When I defined graphOrigin as a computed variable, it convert's the superview's center point to this view's center point and displays the graph in the center of the screen. This does not happen when the variable isn't computed. See code and screenshot for the working case:
class GraphX_YCoordinateView: UIView {
var graphOrigin: CGPoint {
return convertPoint(center, fromView: superview)
}
#IBInspectable var scale: CGFloat = 50 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
// Draw X-Y axes in the view
let axesDrawer = AxesDrawer(contentScaleFactor: contentScaleFactor)
axesDrawer.drawAxesInRect(bounds, origin: graphOrigin, pointsPerUnit: scale)
}
}
AxesDrawer is a class that draws axes in the current view, here is the method signature for drawAxesInRect:
drawAxesInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
And here is the code and screenshot for the case that doesn't work:
class GraphX_YCoordinateView: UIView {
var graphOrigin: CGPoint! {
didSet {
setNeedsDisplay()
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
graphOrigin = convertPoint(center, fromView: superview)
}
#IBInspectable var scale: CGFloat = 50 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
// Draw X-Y axes
let axesDrawer = AxesDrawer(contentScaleFactor: contentScaleFactor)
axesDrawer.drawAxesInRect(bounds, origin: graphOrigin, pointsPerUnit: scale)
}
}
So literally all I changed was initializing the graphOrigin property in place and computing it in the initializer. I didn't touch the StoryBoard at all while editing this code.
I tried initializing the variable inline:
var graphOrigin = convertPoint(center, fromView: superview)
But this wasn't allowed because the implicit self is not initialized when properties are computed.
Can anyone explain why the superview's center seems to change location depending on how the variable is initialized?
This function
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
graphOrigin = convertPoint(center, fromView: superview)
}
means that you are loading view from xib, and at this time the view dimension is 600:600 (look at your xib). So that your graphOrigin = 300:300. This is why you see the second picture.
To fix that problem, you should compute the graphOrigin after the view finish layout in viewDidLayout.
I may be doing something really stupid, but I don't seem to be able to use Interface Builder to connect IBOutlet variables to custom views, but only in Swift.
I've created a class called MyView, which extends from UIView. In my controller, I've got a MyView variable (declared as #IBOutlet var newView: MyView). I go into IB and drag a UIView onto the window and give it a class of MyView.
Whenever I've done similar in Objective C, I'm then able to click on the View Controller button at the top of the app window, select the variable and drag it down to the control to link the two together. When I try it in Swift, it refuses to recognise that the view is there.
If I change the class of the variable in the controller to UIView, it works fine. But not with my custom view.
Has anyone else got this problem? And is it a feature, or just my idiocy?
Code for Controller
import UIKit
class ViewController: UIViewController {
#IBOutlet var newView:MyView
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Code for view
import UIKit
class MyView: UIView {
init(frame: CGRect) {
super.init(frame: frame)
// Initialization code
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect)
{
// Drawing code
}
*/
}
I've had a similar problem, and I think it's partially a caching issue and partially just an Xcode6/Swift issue. The first step I found was required was to make sure that the view controller .swift file would be loaded in the Assistant Editor when choosing "automatic".
With Xcode finding that both the files are linked I could sometimes control-drag from the view/button/etc. from the IB to the .swift file, but often had to drag from the empty circle in the gutter of the #IBOutlet var newView:MyView line to the view I wanted it to match up to.
If you can't get the file to load in the Assistant Editor then I found that doing the following would often work:
Remove the custom class from the IB view
Clean the project (cmd + K)
Close/reopen Xcode
Possibly clean again?
Add the custom class back to the view
Hope it works :)
If that seems to get you half way/nowhere add a comment and I'll see if it triggers anything else I did
In my case import UIKit was missing, after adding this line I could create an IBOutlet from Storyboard again.
I've had a similar problem to the one described in this thread. Maybe you found a solution maybe not but anybody who encounters this in the future. I've found the key is to use the "required init" function as follows:
required init(coder aDecoder: NSCoder) {
print("DrawerView: required init")
super.init(coder: aDecoder)!
screenSize = UIScreen.mainScreen().bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
self.userInteractionEnabled = true
addCustomGestureRecognizer()
}
This is the complete class of my custom view:
import UIKit
import Foundation
class DrawerView: UIView {
var screenSize: CGRect!
var screenWidth: CGFloat!
var screenHeight: CGFloat!
var drawerState: Int = 0
override init (frame : CGRect) {
print("DrawerView: main init")
super.init(frame : frame)
}
override func layoutSubviews() {
print("DrawerView: layoutSubviews")
super.layoutSubviews()
}
convenience init () {
self.init(frame:CGRect.zero)
}
required init(coder aDecoder: NSCoder) {
print("DrawerView: required init")
super.init(coder: aDecoder)!
screenSize = UIScreen.mainScreen().bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
self.userInteractionEnabled = true
addCustomGestureRecognizer()
}
func addCustomGestureRecognizer (){
print("DrawerView: addCustomGestureRecognizer")
let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(self.handleDrawerSwipeGesture(_:)))
swipeDown.direction = UISwipeGestureRecognizerDirection.Down
self.addGestureRecognizer(swipeDown)
let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(self.handleDrawerSwipeGesture(_:)))
swipeUp.direction = UISwipeGestureRecognizerDirection.Up
self.addGestureRecognizer(swipeUp)
print("DrawerView self: \(self)")
}
func minimizeDrawer(){
UIView.animateWithDuration(0.25, delay: 0.0, options: .CurveEaseOut, animations: {
// let height = self.bookButton.frame.size.height
// let newPosY = (self.screenHeight-64)*0.89
// print("newPosY: \(newPosY)")
self.setY(self.screenHeight*0.86)
}, completion: { finished in
self.drawerState = 0
for view in self.subviews {
if let _ = view as? UIButton {
let currentButton = view as! UIButton
currentButton.highlighted = false
} else if let _ = view as? UILabel {
let currentButton = view as! UILabel
if self.tag == 99 {
currentButton.text = "hisotry"
} else if self.tag == 999 {
currentButton.text = "results"
}
}
}
})
}
func handleDrawerSwipeGesture(gesture: UIGestureRecognizer) {
print("handleDrawerSwipeGesture: \(self.drawerState)")
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
switch self.drawerState{
case 0:
if swipeGesture.direction == UISwipeGestureRecognizerDirection.Down {
// nothing to be done, mini and swiping down
print("mini: !")
} else {
// mini and swiping up, should go to underneath city box
UIView.animateWithDuration(0.25, delay: 0.0, options: .CurveEaseOut, animations: {
let toYPos:CGFloat = 128 + 64 + 8
self.setY(toYPos)
}, completion: { finished in
self.drawerState = 1
for view in self.subviews {
if let _ = view as? UIButton {
let currentButton = view as! UIButton
currentButton.highlighted = true
} else if let _ = view as? UILabel {
let currentLabel = view as! UILabel
currentLabel.text = "close"
}
}
})
}
break;
case 1:
if swipeGesture.direction == UISwipeGestureRecognizerDirection.Down {
// open and swiping down
self.minimizeDrawer()
} else {
// open and swiping up, nothing to be done
}
break;
default:
break;
}
}
}
}
Hope this helps...