I am really getting disappointed that's why I ask this question, I found tutorials online for Collapsible TableView but they are with populating the cells with an array of Strings or something similar.
I want to make an Accordion like this with Swift 3,
for some days already I tried a lot of things with UITableViewController because apparently that's the only thing you can make collapsible if you want different cells.
Anyway I started to do it but as I asked my question here I cannot show different UIs in each Cell of each Section.
I am sure there's a way to do it, anyone has any suggestions how to make this work?
I thought maybe there's another way to do it (e.g. with ScrollView or something)
Here is how you could implement it with UIStackView:
Add a vertical UIStackView via storyboard
Add a UIButton as the first subview within the UIStackView
Add some UILabels as the second, third... subview within the UIStackView
Add an IBAction for the UIButton (the first subview)
Implement the IBAction like this:
#IBAction func sectionHeaderTapped(sender: UIButton) {
guard let stackView = sender.superview as? UIStackView else { return }
guard stackView.arrangedSubviews.count > 1 else { return }
let sectionIsCollapsed = stackView.arrangedSubviews[1].hidden
UIView.animateWithDuration(0.25) {
for i in 1..<stackView.arrangedSubviews.count {
stackView.arrangedSubviews[i].hidden = !sectionIsCollapsed
}
}
}
Create multiple UIStackViews like this (you can always use the same IBAction for the UIButton) and embed all of them in a parent (vertical) UIStackView to create a view like in your screenshot.
Feel free to ask if anything is unclear.
Result:
Related
Hello. I am building flights booking app, which has complex UITableViewCell. Basically, it is simple card with shadow, that has bunch of stackviews. First stackview, you see it on image is for labels. It is horizontal and dynamic. The next stackview shows flights. It has complex custom view, but for the sake of simplicity, it is shown with green border. It is also dynamic, so I need separate stackview for it. The next stackview is for airline companies that can handle this booking. I call them as operators. It is also dynamic, so I build yet another stackview for them. And of all these stack views are inside some core stackview. You can ask, why I created separate stack views instead of one? Because, labels above can be hidden. And also spacing in all stackviews are different.
It is really complex design. I followed above approach and build UITableViewCell. But performance is really bad. The reason is simple: I do too many stuff in cellForRowAt. The configure method of UITableViewCell is called everytime when the cell is dequeued. It means I should clean my stackview every time and after only that, append my views. I think it is really affects performance. I don't tell about other if/else statements inside cell. The first question is how can I increase scrolling performance of UITableViewCell in this case?
Some developers reckons that UITableView should be killed. UICollectionView rules the world. OK, but can I use UICollectionView with this design? Yes, of course, but above card would be one UICollectionViewCell and I simply don't avoid problem. The another solution is to build separate UICollectionViewCell for label (see on image), flight and operator. This would definitely increase performance. But, how can I make all of them live inside card?
P.S. What is inside my cellForRowAt method? There is only one configure method and assigning values to closure. But configure method is pretty complex. It gets some protocol which has bunch of computed properties. I pass implementation of that protocol to configure method. Protocol is like this:
protocol Booking {
var flights: [Flight] { get }
var operators: [Operator] { get }
var labels: [Label] { get }
var isExpanded: Bool { get set }
}
Implementation of this protocol is also complex. There are bunch of map functions and if/else statements. Some string manipulations. So, does that cause a problem? How can I solve it? By avoiding properties to be computed and just pass properties(flights, operators) to the implementation?
As I said in my comment, without seeing complete detail, it's tough to help. And, it's a pretty broad question to begin with.
However, this may give you some assistance...
Consider two cell classes. In each, the "basic" elements are added when the cell is created -- these elements will exists regardless of actually cell data:
your "main" stack view
your "labels" stack view
your "flights" stack view
your "operators" stack view
To simplify things, let's just think about the "operators" stack view, and we'll say each "row" is a single label.
What you may be doing now when you set the data in the cell is something like this...
In the cell's init func:
// create your main and 3 sub-stackViews
Then, when you set the data from cellForRowAt:
// remove all labels from operator stack
operatorStack.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
// add new label for each operator
thisBooking.operators.forEach { op in
let v = UILabel()
v.font = .systemFont(ofSize: 15)
v.text = op.name
operatorStack.addArrangedSubview(v)
}
So, each time you dequeue a cell in cellForRowAt and set its data, you are removing all of the "operator" views from the stack view, and then re-creating and re-adding them.
Instead, if you know it will have a maximum of, let's say 10, "operator" subviews, you can add them when the cell is created and then show/hide as needed.
In the cell's init func:
// create your main and 3 sub-stackViews
// add 10 labels to operator stack
// when cell is created
for _ in 1...10 {
let v = UILabel()
v.font = .systemFont(ofSize: 15)
operatorStack.addArrangedSubview(v)
}
Then, when you set the data from cellForRowAt:
// set all labels in operator stack to hidden
operatorStack.arrangedSubviews.forEach {
$0.isHidden = true
}
// fill and unhide labels as needed
for (op, v) in zip(thisBooking.operators, operatorStack.arrangedSubviews) {
guard let label = v as? UILabel else { fatalError("Setup was wrong!") }
label.text = op.name
label.isHidden = false
}
That way, we only create and add "operator views" once - when the cell is created. When it is dequeued / reused, we're simply hiding the unused views.
Again, since you say you have a "really complex design", there is a lot more to consider... and as I mentioned you may need to rethink your whole approach.
However, the basic idea is to only create and add subviews once, then show/hide them as needed when the cell is reused.
It seems like Apple's new feature of auto-flip interface on RTL languages cause problems when using UICollectionView.
I used constraints of type Trailing/Leading for the collection view and they switched their values, as they should, on RTL language.
The problem is that the data actually presented is of the last indexPath in the collection's data source but the UIScrollView.contentOffset.x of the first cell is 0.
A proper behaviour would have been one of the following:
Displaying the first indexPath correctly and switching the direction of the scroll (to the right) - Best option
Not flipping the UI/Constraints so the presented-data / indexPath / scrollView.contentOffset.x will be synchronised - Option that disabling the RTL support.
Presenting cell and data of the last indexPath but fixing the scrollView.contentOffset.x to represent the last cell position also.
I guess Apple might fix it sometime in the future but meanwhile we'll have to use workarounds like reversing array and/or scrolling to the last object.
I was in a similar situation and found a solution for this. If you are using swift, add the following snippet to your project, and it will make sure that the bounds.origin always follows leading edge of the collection view.
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true
}
}
If you are using Objective-C, just subclass the UICollectionViewLayout class, and override flipsHorizontallyInOppositeLayoutDirection, and return true. Use this subclass as the layout object of your collection view.
I am late but if you don't want to create an extension because it will affect all the collection View in our app. Simply create your own custom class ie.
class CustomLayoutForLocalization : UICollectionViewFlowLayout{
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true
}
}
To use this class:
// your way of deciding on whether you need to apply this layout may vary depending on use of other tools like LanguageManager_iOS to handle multi-language support
if myCollectionView.effectiveUserInterfaceLayoutDirection == .rightToLeft {
let customLayout = CustomLayoutForRTL()
// if your elements are variable size use the following line
customLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
// if you want horizontal scroll (single line)
customLayout.scrollDirection = .horizontal
myCollectionView.collectionViewLayout = customLayout
}
There is one common solution for that problem that works for me, follow below steps to overcome that problem,
Give the auto layout constraint as per your requirement and then from attribute inspector change the semantic control property of the collection view to Force right-to-left from the storyboard.
Then open storyboard as source code and find for the “leading” attributes of your relevant collection view and replace that with the “left” and same for the “trailing” replace that with the “right”. Now you almost done.
now that will give you result as per your requirement.
import UIKit
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return UIApplication.shared.userInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.rightToLeft
}
not pretty though simple math does the trick. (for horizontal collectionview)
- (void)switchSemanticDirection:(UISwitch*)sender {
//TEST switch the semantic direction between LTR and RTL.
if (sender.isOn) {
UIView.appearance.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
} else {
UIView.appearance.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
}
[self.myContent removeFromSuperview];
[self.view addSubview:self.myContent];
//reload your collection view to apply RTL setting programmatically
[self.list reloadData];
//position your content into the right offset after flipped RTL
self.list.contentOffset = CGPointMake(self.list.contentSize.width - self.list.contentOffset.x - self.list.bounds.size.width,
self.list.contentOffset.y);
}
Hello
Now I've been trying to display html links in a UITableView.
I've been trying to do this via adding instances of UITableViewCell to the tableview's subview.
func updateViewController(){
for name : String in currentDirectoryNames {
var pusher = UITableViewCell();
pusher.textLabel?.text = name;
listView.addSubview(pusher);
}
}
Sadly the result ends up with the text overlapped in one row :(
It looks like this...
Any ideas?
That's not how UITableView works, at all. I highly suggest you read Apple's documentation on TableView Programming. At the very least, you'll need to set your view controller as the dataSource for the table view and implement the -tableView:numberOfRowsInSection: and -tableView:cellForRowAtIndexPath: methods.
The func you're calling is adding all of your subviews with origin.y equal to 0 (which makes them look like they're all on top of each other). You may have to do something like set pusher.frame.origin.y to dynamically calculate based on which cell is being added.
The problem I'm trying to solve is this: I have a DetailViewController that displays the data for my Model with UIImageView's, UITextFields's, etc.
If the user taps a button, those DetailViewController's views move to different positions and start to be editable. When editable, if the user taps one of the UITextField (just one of them is special) the UITextField moves to the top of the screen and a UITableView appears to autocomplete it (just like when you type something on google).
The user can also tap the same button to go back to the display state (where nothing is editable).
So basically I have some views in a ViewController with 3 possible state: DisplayState, EditingState, EditingWithFocusOnSpecialTextFieldsState.
I'd like to have all those positioning state described by NSLayoutConstraints, and, if possible, just in the storyboard.
One thing I could do is this Animate to a storyboard state / position, but this involves writing every constraint for each state in code, therefore I couldn't visualize them really well in storyboard while developing (Also, writing constraints in code is a lot less maintainable than in storyboard).
What I would like is something like creating 3 different XIBs, for example, or different copies of my DetailViewController in storyboard with the 3 different positions for each of the subviews, and then animate between them.
If it makes any difference, I'm always using the latest iOS version (iOS 8 right now) and Swift.
I do know Objective-C very well too if you don't want to answer in Swift.
As far as I know, there's no way to do this with 3 different views, and get the result you want (at least no straight forward way). One approach would be to make 3 constraints (one for each state) to any one edge of the superview that you need to adjust for each view (in addition to any other constraints you need that you're not going to modify). One would have a high priority (I'm using 900, it can't be 1000), and the other 2 would have a lower priority (499 in my example). When you switch states, you change which of the 3 has the high priority. This does involve making a lot of constraints, and I found that the easiest way to implement the switching in code was to give the constraints identifiers in IB (which you do in the "User Defined Runtime Attributes" section of the Identity Inspector). This approach means I don't have to make IBOutlets for all those constraints. Here is an example with two views in the storyboard,
You can see the text field has 3 constraints to the top, and the image view has 3 centerY constraints (though you can't see that there are 3). Each constraint (in the groups of 3) has its identifier set to "display", "edit", or "focus". I give the one that's called "display" the priority of 900, and the other 2 get 499 (because I want the view to start in display mode). In this test app, I'm using 3 buttons to change the state, though, of course, you could use other means to accomplish that. Here is the code I use to switch the priorities,
enum EditState: String {
case Display = "display" // these strings are the same as the ones assigned to the identifier property of the constraints in IB (in "user defined runtime attributes")
case Editing = "edit"
case EditWithFocus = "focus"
}
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
func updatEditingState(state: EditState) {
var constraintsArray = self.view.constraints() as [NSLayoutConstraint]
constraintsArray += self.imageView.constraints() as [NSLayoutConstraint]
for con in constraintsArray {
if let name = con.identifier? {
if name == "display" || name == "edit" || name == "focus" {
con.priority = (name == state.rawValue) ? 900 : 499
}
}
}
UIView.animateWithDuration(0.5) {self.view.layoutIfNeeded()}
}
#IBAction func EnterEditingState(sender: UIButton) {
updatEditingState(EditState.Editing)
}
#IBAction func enterDisplayStatus(sender: UIButton) {
updatEditingState(EditState.Display)
}
#IBAction func enterFocusStatus(sender: UIButton) {
updatEditingState(EditState.EditWithFocus)
}
}
Ive got one question about using a subView in my custom TableViewCells. On certain rows i want to one or more images, and i am doing this programmatically with:
func addImageToCell(image: UIImage, initialYCoordinate: CGFloat, initialHeight: CGFloat, initialXCoordinate: CGFloat,imageUUID: String) {
imageButton = UIButton.buttonWithType(UIButtonType.Custom) as? UIButton
.....
imageButton!.addTarget(formVC, action: "imageButtonPressed:", forControlEvents: .TouchUpInside)
self.contentView.addSubview(imageButton!)
}
That works fine. But, when i scroll my TableView and it comes to an row with an Image, there is a small "lag". Normally it is totally smooth, but when he is trying to load this cell, there is a (maybe 0,1 seconds) lag in the software.
Is there a better way to do this programmatically without any lags? Ill load my Images on initializing the UITableViewController from a CoreData Fetch.
This is my cellForRowAtIndexPath
if(cellObject.hasImages == true) {
cell.qIndex = indexPath.row
var initialXCoordinate:CGFloat = 344.0
let questionImages = formImages[cellObject.qIndex] as [String: Images]!
for (key,image) in questionImages {
let thumbnailImage = UIImage(data: image.image)
cell.addImageToCell(thumbnailImage, initialYCoordinate: 44.00 ,initialHeight: cellObject.rowheight + cellObject.noticeHeight!, initialXCoordinate: initialXCoordinate, imageUUID: image.imageUUID)
initialXCoordinate += 130
}
}
Any Ideas? Thanks in advance
Edit: Ill use this to prevent the Buttons to get reused:
override func prepareForReuse() {
super.prepareForReuse()
if(self.reuseIdentifier != "CraftInit") {
for item in self.contentView.subviews {
if(item.isKindOfClass(UIButton)) {
item.removeFromSuperview()
}
}
}
The lag is most probably caused by the allocation of new memory for UIButton, adding it to cell's contentView and loading an image into it at the time of the cell being reused. That's one problem.
The other problem is that you're creating a button per every reuse of the cell. Basically, every time you scroll the cell out of the visibility range and scroll it back - you add another button with another image, that also consumes memory and performance.
To make this work properly you need to create a custom table view cell with a maximum amount of images (buttons) pre-rendered and hide the ones you don't need to show.
Do not add / remove subviews while reusing the table view cell, hide and show them instead, it's much faster.
Where are you removing these subviews? Because if you're not removing them, as the user scroll, you'll keep on adding subviews to the same cell over and over again, degrading performance.
My approach usually is to to have such views constructed when the cell is created rather than in cellForRowAtIndexPath, but I'm guessing you can't do that as you don't know the number of images. That said, you could still formulate a strategy where these subviews are added at cell creation, and then reused as per your model.
Another suggestion is to have the UIImage objects created all at once, outside cellForRowAtIndexPath.