I was previously able to clean up my code by adding multiple views (UIImageViews, UILabels, and UIButtons) to an array and then iterating through the array to make a property change like this:
var hideViews = [imageView1, imageView2, label1, button1, button2]
for eachView in hideViews {
eachView.isHidden = true
}
which then became in another version of Swift:
var hideViews = [imageView1, imageView2, label1, button1, button2] as [Any]
for eachView in hideViews {
(eachView as AnyObject).isHidden = true
}
I was also able to use this to move several views at once:
for view in viewsToMove {
(view as AnyObject).frame = CGRect(x: view.frame.origin.x - 30, y: view.frame.origin.y, width: view.frame.width, height: view.frame.height)
}
I am now getting the errors:
Cannot assign to immutable expression of type 'Bool!'
Cannot assign to immutable expression of type 'CGRect!'
Does anybody know what I'm missing here, in order to do this in Swift 3?
Thanks!
Given an array of UIView
let hideViews: [UIView] = ...
You can hide each view
hideViews.forEach { $0.isHidden = true }
move each view 30 points to the left
hideViews.forEach { $0.frame.origin.x -= 30 }
or both
hideViews.forEach {
$0.isHidden = true
$0.frame.origin.x -= 30
}
isHidden and frame are properties of UIView class so you should not cast them to AnyObject if you want to update properties that belong to them. Just do:
let views: [UIView] = [imageView1, imageView2, label1, button1, button2]
for view in views {
view.isHidden = true
view.frame = CGRect(x: ..., y: ...)
}
You don't need to force cast to Any or AnyObject.
This work For Swift3:
let v = UIView()
let i = UIImageView()
let l = UILabel()
let b = UIButton()
// Swift auto infers array type, this is equal to write
// let views: [UIView] = [v, i, l, b]
let views = [v, i, l, b]
views.forEach {
$0.isHidden = true
}
//or
for view in views {
view.frame = CGRect.zero
}
Related
So I have created this custom container view which I am laying out using autolayout constraint.
func configureSegmentContainerView() {
segmentContainerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
segmentContainerView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8).isActive = true
segmentContainerView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8).isActive = true
segmentContainerView.heightAnchor.constraint(equalToConstant: 3).isActive = true
}
In the view controller, the viewDidLoad() is this:
setupDataSource()
segmentContainerView = ATCStorySegmentsView()
view.addSubview(segmentContainerView)
configureSegmentContainerView()
segmentContainerView.translatesAutoresizingMaskIntoConstraints = false
segmentContainerView.numberOfSegments = friendStory.count
Now once the data source is setup and I have the friendStory count, I am assigning it to segmentContainerView.numberofSegments
In segmentContainerview class this is what is happening:
var numberOfSegments: Int? {
didSet {
addSegments()
}
}
In addSegments(), I am adding UIViews depending upon the numberOfSegments this is the code for that:
private func addSegments() {
guard let numberOfSegments = numberOfSegments else { return }
layoutIfNeeded()
setNeedsLayout()
for i in 0..<numberOfSegments {
let segment = Segment()
addSubview(segment.bottomSegment)
addSubview(segment.topSegment)
configureSegmentFrame(index: i, segmentView: segment)
segmentsArray.append(segment)
}
}
private func configureSegmentFrame(index: Int, segmentView: Segment) {
guard let numberOfSegments = numberOfSegments else { return }
let widthOfSegment : CGFloat = (self.frame.width - (padding * CGFloat(numberOfSegments - 1))) / CGFloat(numberOfSegments)
let i = CGFloat(index)
let segmentFrame = CGRect(x: i * (widthOfSegment + padding), y: 0, width: widthOfSegment, height: self.frame.height)
segmentView.bottomSegment.frame = segmentFrame
segmentView.topSegment.frame = segmentFrame
segmentView.topSegment.frame.size.width = 0
}
**Question and Issue: ** Instead of getting 4 UIViews, I am getting 3, but the third one is not correctly placed and is going outside the parent container. How can I get these uiviews aligned correctly. I am guessing there is some issue with where setNeedsLayout() and layoutIfNeeded() needs to be called. Please help.
Segment is a struct with two properties - bottomSegment and topSegment. Both being UIView
You can see how just three UIView segments appear. I needs to 4 (numberOfSegments = 4) of these. Also I am giving the parent container constant of 8 and -8 for right and leftAnchor. so all 4 segments need to be placed within this view. As you can see in the picture above the last segment is going outside of the parent container.
Try to call addSegments at onViewDidAppear. If it works, it means that in your code the view does not still have the correct frame.
For this to work you need to adapt the frames of the views, when the view controller's view changed:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let numberOfSegments = numberOfSegments else { return }
for i in 0..<numberOfSegments {
configureSegmentFrame(index: i, segmentView: segment)
}
}
You will probably have to do it cleaner, but it should work.
The problem is that
let widthOfSegment : CGFloat = (self.frame.width ...
is being called too soon. You cannot do anything in that depends upon self having its frame until after it has its real frame.
For a UIView, that moment is after layoutSubviews has been called for the first time.
I have a "off-screen" UIView hierarchy which I want render in different locations of my screen. In addition it should be possible to show only parts of this view hierarchy and should reflect all changes made to this hierarchy.
The difficulties:
The UIView method drawHierarchy(in:afterScreenUpdates:) always calls draw(_ rect:) and is therefore very inefficient for large hierarchies if you want to incorporate all changes to the view hierarchy. You would have to redraw it every screen update or observe all changing properties of all views. Draw view hierarchy documentation
The UIView method snapshotView(afterScreenUpdates:) also does not help much since I have not found a way to get a correct view hierarchy drawing if this hierarchy is "off-screen". Snapshot view documentation
"Off-Screen": The root view of this view hierarchy is not part of the UI of the app. It has no superview.
Below you can see a visual representation of my idea:
Here's how I would go about doing it. First, I would duplicate the view you are trying to duplicate. I wrote a little extension for this:
extension UIView {
func duplicate<T: UIView>() -> T {
return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self)) as! T
}
func copyProperties(fromView: UIView, recursive: Bool = true) {
contentMode = fromView.contentMode
tag = fromView.tag
backgroundColor = fromView.backgroundColor
tintColor = fromView.tintColor
layer.cornerRadius = fromView.layer.cornerRadius
layer.maskedCorners = fromView.layer.maskedCorners
layer.borderColor = fromView.layer.borderColor
layer.borderWidth = fromView.layer.borderWidth
layer.shadowOpacity = fromView.layer.shadowOpacity
layer.shadowRadius = fromView.layer.shadowRadius
layer.shadowPath = fromView.layer.shadowPath
layer.shadowColor = fromView.layer.shadowColor
layer.shadowOffset = fromView.layer.shadowOffset
clipsToBounds = fromView.clipsToBounds
layer.masksToBounds = fromView.layer.masksToBounds
mask = fromView.mask
layer.mask = fromView.layer.mask
alpha = fromView.alpha
isHidden = fromView.isHidden
if let gradientLayer = layer as? CAGradientLayer, let fromGradientLayer = fromView.layer as? CAGradientLayer {
gradientLayer.colors = fromGradientLayer.colors
gradientLayer.startPoint = fromGradientLayer.startPoint
gradientLayer.endPoint = fromGradientLayer.endPoint
gradientLayer.locations = fromGradientLayer.locations
gradientLayer.type = fromGradientLayer.type
}
if let imgView = self as? UIImageView, let fromImgView = fromView as? UIImageView {
imgView.tintColor = .clear
imgView.image = fromImgView.image?.withRenderingMode(fromImgView.image?.renderingMode ?? .automatic)
imgView.tintColor = fromImgView.tintColor
}
if let btn = self as? UIButton, let fromBtn = fromView as? UIButton {
btn.setImage(fromBtn.image(for: fromBtn.state), for: fromBtn.state)
}
if let textField = self as? UITextField, let fromTextField = fromView as? UITextField {
if let leftView = fromTextField.leftView {
textField.leftView = leftView.duplicate()
textField.leftView?.copyProperties(fromView: leftView)
}
if let rightView = fromTextField.rightView {
textField.rightView = rightView.duplicate()
textField.rightView?.copyProperties(fromView: rightView)
}
textField.attributedText = fromTextField.attributedText
textField.attributedPlaceholder = fromTextField.attributedPlaceholder
}
if let lbl = self as? UILabel, let fromLbl = fromView as? UILabel {
lbl.attributedText = fromLbl.attributedText
lbl.textAlignment = fromLbl.textAlignment
lbl.font = fromLbl.font
lbl.bounds = fromLbl.bounds
}
if recursive {
for (i, view) in subviews.enumerated() {
if i >= fromView.subviews.count {
break
}
view.copyProperties(fromView: fromView.subviews[i])
}
}
}
}
to use this extension, simply do
let duplicateView = originalView.duplicate()
duplicateView.copyProperties(fromView: originalView)
parentView.addSubview(duplicateView)
Then I would mask the duplicate view to only get the particular section that you want
let mask = UIView(frame: CGRect(x: 0, y: 0, width: yourNewWidth, height: yourNewHeight))
mask.backgroundColor = .black
duplicateView.mask = mask
finally, I would scale it to whatever size you want using CGAffineTransform
duplicateView.transform = CGAffineTransform(scaleX: xScale, y: yScale)
the copyProperties function should work well but you can change it if necessary to copy even more things from one view to another.
Good luck, let me know how it goes :)
I'd duplicate the content I wish to display and crop it as I want.
Let's say I have a ContentViewController which carries the view hierarchy I wish to replicate. I would encapsule all the changes that can be made to the hierarchy inside a ContentViewModel. Something like:
struct ContentViewModel {
let actionTitle: String?
let contentMessage: String?
// ...
}
class ContentViewController: UIViewController {
func display(_ viewModel: ContentViewModel) { /* ... */ }
}
With a ClippingView (or a simple UIScrollView) :
class ClippingView: UIView {
var contentOffset: CGPoint = .zero // a way to specify the part of the view you wish to display
var contentFrame: CGRect = .zero // the actual size of the clipped view
var clippedView: UIView?
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
clippedView?.frame = contentFrame
clippedView?.frame.origin = contentOffset
}
}
And a view controller container, I would crop each instance of my content and update all of them each time something happens :
class ContainerViewController: UIViewController {
let contentViewControllers: [ContentViewController] = // 3 in your case
override func viewDidLoad() {
super.viewDidLoad()
contentViewControllers.forEach { viewController in
addChil(viewController)
let clippingView = ClippingView()
clippingView.clippedView = viewController.view
clippingView.contentOffset = // ...
viewController.didMove(to: self)
}
}
func somethingChange() {
let newViewModel = ContentViewModel(...)
contentViewControllers.forEach { $0.display(newViewModel) }
}
}
Could this scenario work in your case ?
I updated my project to Swift 3 and I experienced problems in the following pod: https://github.com/Ramotion/expanding-collection and it no longer works :(
The problems are the following, I will explain each problem:
1- "Operator should no longer be declared with body; use a precedence group instead" yellow warning on the following line in the class "Constraints Helper":
infix operator >>>- { associativity left precedence 150}
How to write this in Swift 3?
2-"'>>>-' produces 'NSLayoutConstraint', not the expected contextual result type 'Void' (aka '()')" red error on the following line in the class "Constraints Helper":
func addScaleToFillConstratinsOnView(_ view: UIView) {
[NSLayoutAttribute.left, .right, .top, .bottom].forEach { attribute in
(self, view) >>>- { $0.attribute = attribute }
}
}
How to fix this issue?
3-"Result of operator '>>>-' is unused" yellow warning on the following lines in the AnimatingBarButton class:
fileprivate func configureImageView(_ imageView: UIImageView, imageName: String) {
guard let customView = customView else { return }
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = UIImage(named: imageName)
imageView.contentMode = .scaleAspectFit
// imageView.backgroundColor = .greenColor()
// imageView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
customView.addSubview(imageView)
//Result of operator '>>>-' is unused - HERE 1
[(NSLayoutAttribute.centerX, 12), (.centerY, -1)].forEach { info in
(customView, imageView) >>>- {
$0.attribute = info.0
$0.constant = CGFloat(info.1)
}
}
//Result of operator '>>>-' is unused - HERE 2
[NSLayoutAttribute.height, .width].forEach { attribute in
imageView >>>- {
$0.attribute = attribute
$0.constant = 20
}
}
}
Also I am getting the same error in this piece of code from the BasePageCollectionCell:
fileprivate func createShadowViewOnView(_ view: UIView?) -> UIView? {
guard let view = view else {return nil}
let shadow = Init(UIView(frame: .zero)) {
$0.backgroundColor = .clear
$0.translatesAutoresizingMaskIntoConstraints = false
$0.layer.masksToBounds = false;
$0.layer.shadowColor = UIColor.black.cgColor
$0.layer.shadowRadius = 10
$0.layer.shadowOpacity = 0.3
$0.layer.shadowOffset = CGSize(width: 0, height:0)
}
contentView.insertSubview(shadow, belowSubview: view)
for info: (attribute: NSLayoutAttribute, scale: CGFloat) in [(NSLayoutAttribute.width, 0.8), (NSLayoutAttribute.height, 0.9)] {
if let frontViewConstraint = view.getConstraint(info.attribute) {
//Result of operator '>>>-' is unused
shadow >>>- {
$0.attribute = info.attribute
$0.constant = frontViewConstraint.constant * info.scale
}
}
}
for info: (attribute: NSLayoutAttribute, offset: CGFloat) in [(NSLayoutAttribute.centerX, 0), (NSLayoutAttribute.centerY, 30)] {
//Result of operator '>>>-' is unused
(contentView, shadow, view) >>>- {
$0.attribute = info.attribute
$0.constant = info.offset
}
}
// size shadow
let width = shadow.getConstraint(.width)?.constant
let height = shadow.getConstraint(.height)?.constant
shadow.layer.shadowPath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: width!, height: height!), cornerRadius: 0).cgPath
return shadow
}
MARK:- PageCollectionView
//Result of operator '>>>-' is unused
collectionView >>>- {
$0.attribute = .height
$0.constant = height
}
How to fix this issue?
I know that this is long, and that the author needs to fix these issues, but the author is not responding or letting us know about an ETA of an update. And I was just about to finish up my app and then updated to Swift 3, I would really appreciate it if you could guide me through this. PS: Many people are facing similar issues in this library, I will share your solutions in the support page of that library.
Best,
Aa
I am trying to figure out how to get the count of an array within an array (do not know the technical term).
I am creating groups of UIViews, and I would like the width of these groups to change dynamically depending on the number of UIViews that are within the group.
The only way I know how to do this (potentially) is to find the count of by blockTitles (group), but I am having issues pulling this count from within my palletViewConstraint() function.
Looking for help with this solution or recommendations for better solutions.
Cheers!
func mainViews(inputView: UIView){
<...>
//POPULATING PALLET WITH BLOCKS
let blockTitles1 = ["1", "2", "3", "4", "5"]
let blockTitles2 = ["6", "7", "8", "9", "10"]
let blockTitles3 = ["11", "12", "13", "14", "15"]
let blockTitles4 = ["16", "17", "18", "19", "20"]
let blockTitles5 = ["21", "22", "23", "24", "25"]
var group1 = createBlockGroup(blockTitles1)
var group2 = createBlockGroup(blockTitles2)
var group3 = createBlockGroup(blockTitles3)
var group4 = createBlockGroup(blockTitles4)
var group5 = createBlockGroup(blockTitles5)
palletView.addSubview(group1)
palletView.addSubview(group2)
palletView.addSubview(group3)
palletView.addSubview(group4)
palletView.addSubview(group5)
palletViewConstraint(palletView, groups: [group1, group2, group3, group4, group5], blockTitles: [blockTitles1, blockTitles2, blockTitles3, blockTitles4, blockTitles5])
}
func createBlock(title: String) -> UIView{
let block = UIView()
block.backgroundColor = UIColor.lightGrayColor()
block.translatesAutoresizingMaskIntoConstraints = false
let blockLabel = UILabel()
blockLabel.frame = CGRectMake(0, 0, 100, 100)
blockLabel.text = title
blockLabel.textColor = UIColor.darkGrayColor()
blockLabel.textAlignment = NSTextAlignment.Center
block.addSubview(blockLabel)
return block
}
func createBlockGroup(blockTitles: [String]) -> UIView {
var blocks = [UIView]()
let blockGroups = UIView()
blockGroups.frame = CGRectMake(0, 0, 100, 100)
blockGroups.backgroundColor = UIColor.redColor()
for blockTitle in blockTitles{
let block = createBlock(blockTitle)
blocks.append(block)
blockGroups.addSubview(block)
}
blockConstraint(blocks, mainView: blockGroups)
return blockGroups
}
func blockConstraint(blocks: [UIView], mainView: UIView){
<...>
}
func palletViewConstraint(inputView: UIView, groups: [UIView], blockTitles: [[String]]){
print(blockTitles.count)
}
func mainViewConstraints(inputView: UIView, pallet: UIScrollView, canvas: UIView){
<...>
}
}
You can access the count of an array within an array by using
mainArr[0].count
This will get the count of the first array in the mainArr.
I would recommend subclassing UIView and using a convenience initializer to set the frame to a value divided by the number of views (which can be accessed by a delegate pattern).
Here is an example of UIView subclass that I have set up to dynamically adjust a grid of subviews. The whole project can be found on my github page: https://github.com/cmako10/UI-View-Groups
Here is the subclass:
class GroupView: UIView {
var rows: Int!
var columns: Int!
var size: CGSize!
var origin: CGPoint!
enum TypeToAdd: String {
case row = "Row"
case column = "Column"
}
enum AlterAction: String {
case add = "Add"
case delete = "Delete"
}
convenience init(origin: CGPoint, size: CGSize, columns: Int, rows: Int) {
self.init(frame: CGRect(origin: origin, size: size))
self.size = size
self.origin = origin
self.rows = rows
self.columns = columns
}
override func drawRect(rect: CGRect) {
frame.size.width = size.width
frame.size.height = size.height
super.drawRect(rect)
for view in self.subviews {
view.removeFromSuperview()
}
let width = size.width / CGFloat(columns)
let height = size.height / CGFloat(rows)
let subviewSize = CGSize(width: width, height: height)
for y in 0..<rows {
for x in 0..<columns {
let newView = UIView(frame: CGRect(origin: CGPoint(x: (CGFloat(x) * width), y: (CGFloat(y) * height)), size: subviewSize))
let label = UILabel(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: subviewSize))
label.adjustsFontSizeToFitWidth = true
label.textAlignment = .Center
label.font = UIFont.systemFontOfSize(15, weight: 10.0)
label.textColor = UIColor.blackColor()
label.center = CGPoint(x: newView.frame.midX, y: newView.frame.midY)
label.text = "[\(x), \(y)]"
newView.addSubview(label)
if y % 2 == 0 {
if x % 2 == 0 {
newView.backgroundColor = UIColor.lightGrayColor()
} else {
newView.backgroundColor = UIColor.blueColor()
}
} else {
if x % 2 == 0 {
newView.backgroundColor = UIColor.blueColor()
} else {
newView.backgroundColor = UIColor.lightGrayColor()
}
}
addSubview(newView)
}
}
}
func alter(type: TypeToAdd, action: AlterAction){
switch type {
case .column:
switch action {
case .add:
columns = columns + 1
case .delete:
columns = columns - 1
}
case .row:
switch action {
case .add:
rows = rows + 1
case .delete:
rows = rows - 1
}
}
setNeedsDisplay()
}
}
You should download and examine the Xcode project. The general idea to dynamically update the UIView is to remove all subviews and then re-add the subviews in drawRect and then call setNeedDisplay whenever you need to update the contents of the GroupView.
Edit:
To access the counts of the subarrays of blockTitles I see two possible solutions.
Use a for loop, define a constant at the beginning of the loop to be equal to the count of the subarray at "i", then put the code inside palletViewConstraint after that and reference the count constant as needed.
Define a variable to hold the count values (an array of Ints) at the start of palletViewConstraint use the high-level function map to append all the blockTitle counts to the previously defined variable, use a for loop and the defined count array to run the rest of your palletViewConstraint.
Examples:
Example 1:
func palletViewConstraint(inputView: UIView, groups: [UIView], blockTitles: [[String]]){
//Do this so the count is not recalculated each for loop
let blockTitlesCount = blockTitles.count
for i in blockTitlesCount {
let subArrCount = blockTitles[i].count
//The rest of your palletViewConstraint Code that references subArrCount
}
}
Example 2:
func palletViewConstraint(inputView: UIView, groups: [UIView], blockTitles: [[String]]){
var countArr = [Int]()
arr.map { (subArr) in
countArr.append(subArr.count)
}
for count in countArr {
//Your code referencing the count of each subArr
}
}
If you need elaboration please ask!
In my ViewDidLoad:
let spiralDimension = CGFloat(ScreenWidth! * 0.10)
let spiralName = "spiral.png"
let spiralImage = UIImage(named: spiralName)
spiralView = UIImageView(image: spiralImage!)
spiralView!.frame = CGRect(x: ScreenWidth! / 2 - spiralDimension/2, y: (ScreenHeight!-TabBarHeight!) / 2 - spiralDimension/2 , width: spiralDimension, height: spiralDimension)
spiralView!.tintColor = UIColor.redColor()
self.view.addSubview(spiralView!) //works!!!
Somewhere later...
func fadeBackground(){
UIView.animateWithDuration(self.fadeTime, delay: 0, options: UIViewAnimationOptions.AllowUserInteraction, animations: {
var randomIndex = Int(arc4random_uniform(UInt32(CONSTANTS.MainColorScheme.count)))
var subviews = self.view.subviews
for v in subviews{
if v.isKindOfClass(UIImageView){
println(v) //correctly prints my spiral!
v.tintColor = CONSTANTS.MainColorScheme[randomIndex] //can't do it. XCode won't even auto-complete
}
}
}) { (stuff Bool) -> Void in
}
}
I can't assign tintColor to v, even though it prints my class correctly. Can't build successfully.
subviews returns an array of AnyObject. Thus, you need to cast v in order to set tintColor.
Try:
for v in subviews{
if let v = v as? UIImageView {
println(v)
v.tintColor = CONSTANTS.MainColorScheme[randomIndex]
}
}
You have to typecast it.
if v.isKindOfClass(UIImageView){
let iv = v as! UIImageView
v.tintColor = ...
The problem is that the subviews property is defined in iOS as a variable of type [AnyObject] so Swift doesn't know that your subviews are members of UIView, which has the property tintColor.