`setCollectionViewLayout` animation broken when also changing collection view frame
I got it working by using a custom UICollectionViewFlowLayout
class (link to demo repo). The center cell even stays centered across both layouts (which was actually the exact purpose of the "Bonus points" part in my question)!
Here's my view controller. Instead of conforming to UICollectionViewDelegateFlowLayout
, I now use the custom closures sizeForListItemAt
and sizeForStripItemAt
.
class ViewController: UIViewController {
var isExpanded = false
lazy var listLayout = FlowLayout(layoutType: .list)
lazy var stripLayout = FlowLayout(layoutType: .strip)
@IBOutlet weak var collectionView: UICollectionView!
@IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
@IBAction func toggleExpandPressed(_ sender: Any) {
isExpanded.toggle()
if isExpanded {
collectionView.setCollectionViewLayout(listLayout, animated: true)
collectionViewHeightConstraint.constant = 300
} else {
collectionView.setCollectionViewLayout(stripLayout, animated: true)
collectionViewHeightConstraint.constant = 60
}
UIView.animate(withDuration: 0.6) {
self.view.layoutIfNeeded()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = stripLayout
collectionView.dataSource = self
/// use these instead of `UICollectionViewDelegateFlowLayout`
listLayout.sizeForListItemAt = { [weak self] indexPath in
return CGSize(width: self?.collectionView.frame.width ?? 100, height: 50)
}
stripLayout.sizeForStripItemAt = { indexPath in
return CGSize(width: 100, height: 50)
}
}
}
/// sample data source
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath)
cell.contentView.layer.borderWidth = 5
cell.contentView.layer.borderColor = UIColor.red.cgColor
return cell
}
}
Then, here's my custom UICollectionViewFlowLayout
class. Inside prepare
, I manually calculate and set the frames of each cell. I'm not exactly sure why this works, but the system is now able to figure out which cells are equal to which, even across multiple FlowLayout
s (listLayout
and stripLayout
.).
enum LayoutType {
case list
case strip
}
class FlowLayout: UICollectionViewFlowLayout {
var layoutType: LayoutType
var sizeForListItemAt: ((IndexPath) -> CGSize)? /// get size for list item
var sizeForStripItemAt: ((IndexPath) -> CGSize)? /// get size for strip item
var layoutAttributes = [UICollectionViewLayoutAttributes]() /// store the frame of each item
var contentSize = CGSize.zero /// the scrollable content size of the collection view
override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
override func prepare() { /// configure the cells' frames
super.prepare()
guard let collectionView = collectionView else { return }
let itemCount = collectionView.numberOfItems(inSection: 0) /// I only have 1 section
if layoutType == .list {
var y: CGFloat = 0 /// y position of each cell, start at 0
for itemIndex in 0..<itemCount {
let indexPath = IndexPath(item: itemIndex, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(
x: 0,
y: y,
width: sizeForListItemAt?(indexPath).width ?? 0,
height: sizeForListItemAt?(indexPath).height ?? 0
)
layoutAttributes.append(attributes)
y += attributes.frame.height /// add height to y position, so next cell becomes offset
} /// use first item's width
contentSize = CGSize(width: sizeForStripItemAt?(IndexPath(item: 0, section: 0)).width ?? 0, height: y)
} else {
var x: CGFloat = 0 /// z position of each cell, start at 0
for itemIndex in 0..<itemCount {
let indexPath = IndexPath(item: itemIndex, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(
x: x,
y: 0,
width: sizeForStripItemAt?(indexPath).width ?? 0,
height: sizeForStripItemAt?(indexPath).height ?? 0
)
layoutAttributes.append(attributes)
x += attributes.frame.width /// add width to z position, so next cell becomes offset
} /// use first item's height
contentSize = CGSize(width: x, height: sizeForStripItemAt?(IndexPath(item: 0, section: 0)).height ?? 0)
}
}
/// pass attributes to the collection view flow layout
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutAttributes[indexPath.item]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributes.filter { rect.intersects($0.frame) }
}
/// initialize with a layout
init(layoutType: LayoutType) {
self.layoutType = layoutType
super.init()
}
/// boilerplate code
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true }
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
Thanks to this amazing article for the huge help.
How to animate collection view layout change while using `layoutAttributesForElements`?
Whew! This was a workout. I was able to modify your FlowLayout
so that there are no hiccups in animation. See below.
It works!
Problem
This is what was happening. When you change layouts, the layoutAttributesForElements
method in FlowLayout
is called twice if the content offset of the collection view is anything but (0, 0)
.
This is because you have overridden 'shouldInvalidateLayout' to return true
regardless of whether it is actually needed. I believe the UICollectionView
calls this method on the layout before and after the layout change (as per the observation).
The side effect of this is that your scale transform is applied twice - before and after the animations to the visible layout attributes.
Unfortunately, the scale transform is applied based on the contentOffset
of the collection view (link)
let visibleRect = CGRect(
origin: collectionView.contentOffset,
size: collectionView.frame.size
)
During layout changes the contentOffset
is not consistent. Before the animation starts contentOffset
is applicable to the previous layout. After the animation, it is relative to the new layout. Here I also noticed that without a good reason, the contentOffset "jumps" around (see note 1)
Since you use the visibleRect to query the set of Layout Attributes to apply the scale on, it introduces further errors.
Solution
I was able to find a solution by applying these changes.
- Write helpers methods to transform the content offset (and dependent visibleRect) left by the previous layout to values meaningful for this layout.
- Prevent redundant layout attribute calculates in
prepare
method - Track when and when not the layout is animating
// In Flow Layout
class FlowLayout: UICollectionViewFlowLayout {
var animating: Bool = false
// ...
}
// In View Controller,
isExpanded.toggle()
if isExpanded {
listLayout.reset()
listLayout.animating = true // <--
// collectionView.setCollectionViewLayout(listLayout)
} else {
stripLayout.reset()
stripLayout.animating = true // <--
// collectionView.setCollectionViewLayout(stripLayout)
}
- Override
targetContentOffset
method to handle content offset changes (prevent jumps)
// In Flow Layout
class FlowLayout: UICollectionViewFlowLayout {
var animating: Bool = false
var layoutType: LayoutType
// ...
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard animating else {
// return super
}
// Use our 'graceful' content content offset
// instead of arbitrary "jump"
switch(layoutType){
case .list: return transformCurrentContentOffset(.fromStripToList)
case .strip: return transformCurrentContentOffset(.fromListToStrip)
}
}
// ...
The implementation of content offset transforming is as follows.
/**
Transforms this layouts content offset, to the other layout
as specified in the layout transition parameter.
*/
private func transformCurrentContentOffset(_ transition: LayoutTransition) -> CGPoint{
let stripItemWidth: CGFloat = 100.0
let listItemHeight: CGFloat = 50.0
switch(transition){
case .fromStripToList:
let numberOfItems = collectionView!.contentOffset.x / stripItemWidth // from strip
var newPoint = CGPoint(x: 0, y: numberOfItems * CGFloat(listItemHeight)) // to list
if (newPoint.y + collectionView!.frame.height) >= contentSize.height{
newPoint = CGPoint(x: 0, y: contentSize.height - collectionView!.frame.height)
}
return newPoint
case .fromListToStrip:
let numberOfItems = collectionView!.contentOffset.y / listItemHeight // from list
var newPoint = CGPoint(x: numberOfItems * CGFloat(stripItemWidth), y: 0) // to strip
if (newPoint.x + collectionView!.frame.width) >= contentSize.width{
newPoint = CGPoint(x: contentSize.width - collectionView!.frame.width, y: 0)
}
return newPoint
}
}
There are some minor details I left out in the comments and as a pull request to OP's demo project so anyone interested can study it.
The key take-aways are,
Use
targetContentOffset
when arbitrary changes in content offset occur in response to layout changes.Be careful about incorrect query of layout attributes in
layoutAttributesForElements
. Debug your rects!Remember to clear your cached layout attributes on the
prepare()
method.
Notes
The "jump" behavior is evident even before you introduced scale transforms as seen in your gif.
I sincerely apologize if the answer is lengthy. Or, The solution is not quite what you wanted. The question was interesting which is why I spent the whole day trying to find a way to help.
Fork and Pull request.
Calling setCollectionViewLayout:animated does not reload UICollectionView
-setCollectionViewLayout:
or -setCollectionViewLayout:animated:
won't cause your UICollectionView
reload its data.
If you want to change your cell style or update the data, call
[self.collectionView reloadData]
andUICollectionViewDataSource
protocol methods will be called.If you want to change your
UICollectionView
layout to another, call-setCollectionViewLayout:
. Or if you want to update yourUICollectionView
layout, just call[self.collectionView.collectionViewLayout invalidateLayout]
.
Cell layout and data are two different things in UICollectionView
.
Update
you should discard all the stale data, probably by -reloadData
. And calculate the new frames for every cell in your UICollectionViewLayout subclasses. Override - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
and - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
.
You decide the new contentOffset
. For instance, you could keep the indexPath of some visible cell and decide to scroll to that indexPath
or setContentOffset:
to the frame.origin
of the new cell at the same indexPath
after your layout changed.
- And I find this answer may give you help.
- Also check the awesome video WWDC 2012 Advanced Collection Views and Building Custom Layouts
Invisible Cell CollectionView Animation
Couple issues you're hitting here.
First, you're using a custom collection view layout to position the cells, but then you're also explicitly setting the cell y-origins.
Second, a collection view will only render the visible cells, so when you try to "un-expand" the layout, the bottom cell(s) don't exist.
I'm going to suggest a slightly different approach.
Use a protocol/delegate pattern so your custom UICollectionViewLayout
can tell the controller to expand / collapse the layout when the pinch gesture occurs. The controller will then create a NEW instance of the custom layout and call .setCollectionViewLayout(...)
- wrapped in an animation block - to either expand or collapse.
In addition, the controller will temporarily extend the height of the collection view so the "off-screen" cells will be rendered.
Here's some example code - I really made very few changes to your existing custom layout. The comments I included should be enough to make things clear.
Note, though, that this is Example Code Only -- it has not been thoroughly test and is intended to be a starting point:
protocol PinchProtocol: AnyObject {
func toggleExpanded(_ expand: Bool)
}
class MyWalletVC: UIViewController, PinchProtocol {
var data: [UIColor] = [
.red, .green, .blue, .cyan, .magenta,
//.yellow, .orange, .systemYellow,
]
var collectionView: UICollectionView!
var cvBottom: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
let lay = WalletStackLayout()
lay.isExpanded = false
lay.pinchDelegate = self
collectionView = UICollectionView(frame: .zero, collectionViewLayout: lay)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
let g = view.safeAreaLayoutGuide
cvBottom = collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
cvBottom,
])
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "c")
collectionView.dataSource = self
collectionView.delegate = self
}
func toggleExpanded(_ expand: Bool) {
// increase collection view height
// so "off-screen" cells will be rendered
// I just picked a value of 800 to make sure it's enough
self.cvBottom.constant = 800
UIView.animate(
withDuration: 0.5,
delay: 0,
options: .curveEaseOut,
animations: { [weak self] in
guard let self = self else { return }
// create a NEW layout object
let lay = WalletStackLayout()
// set its isExpanded property
lay.isExpanded = expand
// set self as its pinchDelegate
lay.pinchDelegate = self
// set the new layout
// use "animated: false" because we're animating it with UIView.animate
self.collectionView.setCollectionViewLayout(lay, animated: false)
},
completion: { [weak self] (_: Bool) in
guard let self = self else { return }
// reset collection view height
self.cvBottom.constant = 0
}
)
}
}
extension MyWalletVC: UICollectionViewDataSource, UICollectionViewDelegate {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath)
c.contentView.backgroundColor = data[indexPath.item]
c.contentView.layer.cornerRadius = 16
return c
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Selected item from Tap gesture:", indexPath)
}
}
typealias CellAndLayoutAttributes = (cell: UICollectionViewCell, layoutAttributes: UICollectionViewLayoutAttributes)
class WalletStackLayout: UICollectionViewLayout {
// so we can tell the controller we got pinched
weak var pinchDelegate: PinchProtocol?
// expanded / collapsed layout
var isExpanded: Bool = false
private let heightRatio: CGFloat = 196/343
private let sidePadding: CGFloat = 16.0
private let peekStack: CGFloat = 40
private var cellWidth: CGFloat {
return UIScreen.main.bounds.width - sidePadding * 2.0
//return Device.screenWidth - sidePadding*2
}
private var cellHeight: CGFloat {
return heightRatio * cellWidth
}
private var isMoving: Bool = false
private var collectionLayoutAttributes: [UICollectionViewLayoutAttributes] = []
private var tapGestureRecognizer: UITapGestureRecognizer?
private var pinchGestureRecognizer: UIPinchGestureRecognizer?
// this is needed to keep the Top cell at the Top of the collection view
// when changing the layout
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return .zero
}
override var collectionViewContentSize: CGSize {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return CGSize(width: 0, height: 0)
}
var contentHeight: CGFloat = 0
for index in 0..<collectionView.numberOfSections {
contentHeight += calculateSectionCardHeight(section: index)
}
return CGSize(
width: collectionView.bounds.width,
height: contentHeight
)
}
override func prepare() {
super.prepare()
collectionLayoutAttributes.removeAll()
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return
}
initializeCardCollectionViewLayout()
collectionLayoutAttributes = makeCardsLayoutAttributes()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return nil
}
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
var r = rect
r.size.height += 500
for attributes in collectionLayoutAttributes where attributes.frame.intersects(r) {
visibleLayoutAttributes.append(attributes)
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return collectionLayoutAttributes[indexPath.row]
}
private func getCell(at indexPath: IndexPath) -> UICollectionViewCell? {
return collectionView?.cellForItem(at: indexPath)
}
private func getLayoutAttributes(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return collectionView?.layoutAttributesForItem(at: indexPath)
}
private func getCellAndLayoutAttributes(at indexPath: IndexPath) -> CellAndLayoutAttributes? {
guard let cell = getCell(at: indexPath),
let layoutAttributes = getLayoutAttributes(at: indexPath) else {
return nil
}
return (cell: cell, layoutAttributes: layoutAttributes)
}
// MARK: - BEGIN SET CARDS -
private func makeCardsLayoutAttributes() -> [UICollectionViewLayoutAttributes] {
guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
return []
}
var collectionViewLayout: [UICollectionViewLayoutAttributes] = []
for section in 0..<collectionView.numberOfSections {
for row in 0..<collectionView.numberOfItems(inSection: section) {
let indexPath = IndexPath(row: row, section: section)
collectionViewLayout.append(makeCardLayoutAttributes(forCellWith: indexPath))
}
}
return collectionViewLayout
}
private func makeInitialLayoutAttributes(forCellWith indexPath: IndexPath, height: CGFloat) -> UICollectionViewLayoutAttributes {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let preferredSize = CGSize(width: cellWidth, height: height)
attributes.size = preferredSize
return attributes
}
private func makeCardLayoutAttributes(forCellWith indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let attributes = makeInitialLayoutAttributes(forCellWith: indexPath, height: cellHeight)
let coordinateY = calculateSectionYCoordinate(indexPath: indexPath)
attributes.frame.origin.y = coordinateY
attributes.frame.origin.x = sidePadding
attributes.zIndex = indexPath.item
return attributes
}
private func calculateSectionYCoordinate(indexPath: IndexPath) -> CGFloat {
var sectionYCoordinate: CGFloat = 0
for section in 0..<indexPath.section {
sectionYCoordinate += calculateSectionCardHeight(section: section)
}
if isExpanded {
return (cellHeight + sidePadding) * CGFloat(indexPath.row) + sectionYCoordinate
} else {
return peekStack * CGFloat(indexPath.row) + sectionYCoordinate
}
}
private func calculateSectionCardHeight(section: Int) -> CGFloat {
guard let numberOfItems = collectionView?.numberOfItems(inSection: section) else {
return 0
}
if isExpanded {
let totalExpandedCards: Int = numberOfItems
return (cellHeight + sidePadding) * CGFloat(totalExpandedCards)
} else {
let visibleCardCount: Int = 1
let totalStackedCards: Int = numberOfItems > 1 ? numberOfItems - visibleCardCount : 0
return peekStack * CGFloat(totalStackedCards) + cellHeight + sidePadding
}
}
// MARK: - TAP GESTURE -
private func initializeCardCollectionViewLayout() {
if tapGestureRecognizer == nil {
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGestureHandler))
if let tapGesture = tapGestureRecognizer {
collectionView?.addGestureRecognizer(tapGesture)
}
}
if pinchGestureRecognizer == nil {
pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinchGesture(_:)))
pinchGestureRecognizer?.delegate = self
if let pinchGesture = pinchGestureRecognizer {
collectionView?.addGestureRecognizer(pinchGesture)
}
}
}
}
extension WalletStackLayout: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc private func handlePinchGesture(_ pinchGesture: UIPinchGestureRecognizer) {
if pinchGesture.state == .began || pinchGesture.state == .changed {
guard let collectionView = collectionView,
let tapLocation = pinchGestureRecognizer?.location(in: collectionView),
let indexPath = collectionView.indexPathForItem(at: tapLocation),
!isMoving else {
return
}
if pinchGesture.scale > 1 {
// tell the controller to switch to Expanded layout
pinchDelegate?.toggleExpanded(true)
} else if pinchGesture.scale < 1 {
// tell the controller to switch to Collapsed layout
pinchDelegate?.toggleExpanded(false)
}
}
}
@objc
private func tapGestureHandler() {
guard let collectionView = collectionView,
let tapLocation = tapGestureRecognizer?.location(in: collectionView),
let indexPath = collectionView.indexPathForItem(at: tapLocation) else {
return
}
print("TapGestureHandler Section: \(indexPath.section) Row: \(indexPath.row)")
collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
}
}
No @IBOutlet
or @IBAction
connections needed. Just assign the custom class of a standard view controller to MyWalletVC
and it should run without problem.
Several animations in a collection view
Eventually, I managed to do it myself. In case it will be useful for anyone - one of the possible solutions is the following:
First, create a type of animation, for example var animationType = 1
.
Then, in extension, make some cases:
extension TargetCollectionViewCell: ScaleTransformView {
var scaleOptions: ScaleTransformViewOptions {
if animationType == 1 {
return ScaleTransformViewOptions(
minScale: 0.01,
maxScale: 0.01,
scaleRatio: 0,
translationRatio: .zero,
minTranslationRatio: .zero,
maxTranslationRatio: .zero,
shadowEnabled: false,
rotation3d: .init(angle: 0.8, minAngle: -90, maxAngle: 90, x: 0, y: 1, z: 0, m34: -0.001),
translation3d: .init(translateRatios: (0, -1, 0), minTranslateRatios: (0, 0, 1.25), maxTranslateRatios: (0, 0, 1.25)))
} else if animationType == 2 {
return ScaleTransformViewOptions(
minScale: 0.04,
maxScale: 0.04,
scaleRatio: 0,
translationRatio: .zero,
minTranslationRatio: .zero,
maxTranslationRatio: .zero,
shadowEnabled: false,
rotation3d: .init(angle: 0.8, minAngle: -90, maxAngle: 90, x: 0, y: 1, z: 0, m34: -0.001),
translation3d: .init(translateRatios: (0, -1, 0), minTranslateRatios: (0, 0, 1.25), maxTranslateRatios: (0, 0, 1.25)))
} else {
return ScaleTransformViewOptions(
minScale: 0.07,
maxScale: 0.07,
scaleRatio: 0,
translationRatio: .zero,
minTranslationRatio: .zero,
maxTranslationRatio: .zero,
shadowEnabled: false,
rotation3d: .init(angle: 0.8, minAngle: -90, maxAngle: 90, x: 0, y: 1, z: 0, m34: -0.001),
translation3d: .init(translateRatios: (0, -1, 0), minTranslateRatios: (0, 0, 1.25), maxTranslateRatios: (0, 0, 1.25)))
}
return ScaleTransformViewOptions(
)
}
}
Related Topics
Exc_Bad_Access Using Ibinspectable
How to Get the User "Name" Using Swift
Rotate a Imageview Around a Pivot Point in iOS
How to Get All the Sundays in Array of Date iOS
Error When Decoding Certain Base64 Strings, But Not Others
Setting Uiimageview Image Affects Layout Constraints
iOS Media Playback Controls Notification
Icloud Document Picker from Wkwebview Dismissing Container View
Swift Nfc Mifare - Nfciso7816Apdu Sendmifare Command Not Supported
Settitletextattributes Doesn't Work for Uitabbaritem When It Is Unselected in Swift
Core Data Taking Time to Insert Records with Fetching Entity & Set as Relationship
Pass Data from View Controller to Child Controller in Swift
How to Get the Nondecoded Attributes from a Decoder Container in Swift 4
Get Latitude and Longitude Center of Google Map
Openurl in Appdelegate Conversion Error Nsstring -> String (Swift & iOS8)