I want to implement drag and drop feature with UICollectionViewDragDelegate, UICollectionViewDropDelegate it works pretty well but I need to shrink the underneath cells while lifting one cell with drag behavior and shrink back to original size when user drop the cell back to the cells. But somehow I can't get a stable result with the code like below. The animation is weird and the behavior because unpredictable when trying to drag the cell on the edge of the screen. How can I make the result more predictable?
import UIKit
class ViewController: UIViewController {
#IBOutlet var collectionView: UICollectionView! {
didSet {
collectionView.delegate = self
collectionView.dataSource = self
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
}
}
var items: [UIColor] = [.green, .blue, .brown, .cyan, .red, .magenta, .yellow, .systemPink]
var isShrink: Bool = false {
didSet {
//collectionView.collectionViewLayout.invalidateLayout()
collectionView.performBatchUpdates({}, completion: { _ in })
}
}
}
extension ViewController: UIGestureRecognizerDelegate {}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout _: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
isShrink ? CGSize(width: 60, height: 60) : CGSize(width: 80, height: 60)
}
}
class Cell: UICollectionViewCell {
var shrink: ((Bool) -> Void)?
required init?(coder: NSCoder) {
super.init(coder: coder)
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressed(gesture:)))
addGestureRecognizer(gesture)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//shrink?(true)
print("touchesBegan")
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesEnd")
}
override init(frame: CGRect) {
super.init(frame: frame)
}
#objc func longPressed(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
print("began")
case .ended:
print("ended")
default:
print("default")
}
}
// weak var coordinator: DragCoordinator?
//
override func dragStateDidChange(_ dragState: UICollectionViewCell.DragState) {
super.dragStateDidChange(dragState)
switch dragState {
case .dragging:
shrink?(true)
case .lifting:
shrink?(true)
case .none:
shrink?(false)
#unknown default:
fatalError()
}
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
cell.shrink = { [weak self] isShrink in
self?.isShrink = isShrink
}
cell.backgroundColor = items[indexPath.row]
return cell
}
}
extension ViewController: UICollectionViewDragDelegate {
func collectionView(_ cv: UICollectionView,
itemsForBeginning _: UIDragSession,
at indexPath: IndexPath) -> [UIDragItem] {
[.init(itemProvider: .init(object: "\(indexPath.row)" as NSString))]
}
}
extension ViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView,
performDropWith coordinator: UICollectionViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath else {
return
}
let item = coordinator.items[0]
switch coordinator.proposal.operation {
case .move:
if let sourceIndexPath = item.sourceIndexPath {
collectionView.performBatchUpdates({
let item = items[sourceIndexPath.row]
items.remove(at: sourceIndexPath.row)
items.insert(item, at: destinationIndexPath.row)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
})
}
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
default:
return
}
}
func collectionView(_: UICollectionView,
dropSessionDidUpdate session: UIDropSession,
withDestinationIndexPath _: IndexPath?) -> UICollectionViewDropProposal {
guard session.localDragSession != nil else {
return .init(operation: .copy, intent: .insertAtDestinationIndexPath)
}
guard session.items.count == 1 else {
return .init(operation: .cancel)
}
return .init(operation: .move, intent: .insertAtDestinationIndexPath)
}
}
Related
I have a view controller in my app that plays user videos and you can scroll to the right to go to the next video. I have a cell set up that loads the video from the URL in firebase storage and loads the other data. Here is the cell code:
import UIKit
import AVFoundation
protocol ClipsCollectionViewCellDelegate: AnyObject {
func didTapProfile(with model: VideoModel)
func didTapShare(with model: VideoModel)
func didTapNewClip(with model: VideoModel)
}
class ClipsCollectionViewCell: UICollectionViewCell {
static let identifier = "ClipsCollectionViewCell"
// Labels
private let usernameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = UIColor.systemPink.withAlphaComponent(0.5)
label.backgroundColor = UIColor.systemPink.withAlphaComponent(0.1)
label.clipsToBounds = true
label.layer.cornerRadius = 8
return label
}()
// Buttons
private let profileButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "person.circle"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 32
button.isUserInteractionEnabled = true
return button
}()
private let shareButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "square.and.arrow.down"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 4
button.isUserInteractionEnabled = true
return button
}()
private let newClipButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "plus"), for: .normal)
button.tintColor = .systemOrange
button.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 25
button.isUserInteractionEnabled = true
return button
}()
private let videoContainer = UIView()
// Delegate
weak var delegate: ClipsCollectionViewCellDelegate?
// Subviews
var player: AVPlayer?
private var model: VideoModel?
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .black
contentView.clipsToBounds = true
addSubviews()
}
private func addSubviews() {
contentView.addSubview(videoContainer)
contentView.addSubview(usernameLabel)
contentView.addSubview(profileButton)
contentView.addSubview(shareButton)
contentView.addSubview(newClipButton)
// Add actions
profileButton.addTarget(self, action: #selector(didTapProfileButton), for: .touchUpInside)
shareButton.addTarget(self, action: #selector(didTapShareButton), for: .touchUpInside)
newClipButton.addTarget(self, action: #selector(didTapNewClipButton), for: .touchUpInside)
videoContainer.clipsToBounds = true
contentView.sendSubviewToBack(videoContainer)
}
#objc private func didTapProfileButton() {
guard let model = model else {
return
}
delegate?.didTapProfile(with: model)
}
#objc private func didTapShareButton() {
guard let model = model else {
return
}
delegate?.didTapShare(with: model)
}
#objc private func didTapNewClipButton() {
guard let model = model else {
return
}
delegate?.didTapNewClip(with: model)
}
override func layoutSubviews() {
super.layoutSubviews()
videoContainer.frame = contentView.bounds
let size = contentView.frame.size.width/7
let width = contentView.frame.size.width
let height = contentView.frame.size.height
// Labels
usernameLabel.frame = CGRect(x: (width-(size*3))/2, y: height-880-(size/2), width: size*3, height: size)
// Buttons
profileButton.frame = CGRect(x: width-(size*7), y: height-850-size, width: size, height: size)
shareButton.frame = CGRect(x: width-size, y: height-850-size, width: size, height: size)
newClipButton.frame = CGRect(x: width-size-10, y: height-175-size, width: size/1.25, height: size/1.25)
}
override func prepareForReuse() {
super.prepareForReuse()
usernameLabel.text = nil
player?.pause()
player?.seek(to: CMTime.zero)
}
public func configure(with model: VideoModel) {
self.model = model
configureVideo()
// Labels
usernameLabel.text = model.username
}
private func configureVideo() {
guard let model = model else {
return
}
guard let url = URL(string: model.videoFileURL) else { return }
player = AVPlayer(url: url)
let playerView = AVPlayerLayer()
playerView.player = player
playerView.frame = contentView.bounds
playerView.videoGravity = .resizeAspect
videoContainer.layer.addSublayer(playerView)
player?.volume = 0
player?.play()
player?.actionAtItemEnd = .none
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the view controller code:
import UIKit
struct VideoModel {
let username: String
let videoFileURL: String
}
class BetaClipsViewController: UIViewController, UICollectionViewDelegate {
private var collectionView: UICollectionView?
private var data = [VideoModel]()
/// Notification observer
private var observer: NSObjectProtocol?
/// All post models
private var allClips: [(clip: Clip, owner: String)] = []
private var viewModels = [[ClipFeedCellType]]()
override func viewDidLoad() {
super.viewDidLoad()
title = ""
// for _ in 0..<10 {
// let model = VideoModel(username: "#CJMJM",
// videoFileURL: "https://firebasestorage.googleapis.com:443/v0/b/globe-e8b7f.appspot.com/o/clipvideos%2F1637024382.mp4?alt=media&token=c12d0481-f834-4a17-8eee-30595bdf0e8b")
// data.append(model)
// }
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: view.frame.size.width,
height: view.frame.size.height)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView?.register(ClipsCollectionViewCell.self,
forCellWithReuseIdentifier: ClipsCollectionViewCell.identifier)
collectionView?.isPagingEnabled = true
collectionView?.delegate = self
collectionView?.dataSource = self
view.addSubview(collectionView!)
fetchClips()
observer = NotificationCenter.default.addObserver(
forName: .didPostNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.viewModels.removeAll()
self?.fetchClips()
}
self.collectionView?.reloadData()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
private func fetchClips() {
guard let username = UserDefaults.standard.string(forKey: "username") else {
return
}
let userGroup = DispatchGroup()
userGroup.enter()
var allClips: [(clip: Clip, owner: String)] = []
DatabaseManager.shared.users(for: username) { usernames in
defer {
userGroup.leave()
}
let users = usernames + [username]
for current in users {
userGroup.enter()
DatabaseManager.shared.clips(for: current) { result in
DispatchQueue.main.async {
defer {
userGroup.leave()
}
switch result {
case .success(let clips):
allClips.append(contentsOf: clips.compactMap({
(clip: $0, owner: current)
}))
case .failure:
break
}
}
}
}
}
userGroup.notify(queue: .main) {
let group = DispatchGroup()
self.allClips = allClips
allClips.forEach { model in
group.enter()
self.createViewModel(
model: model.clip,
username: model.owner,
completion: { success in
defer {
group.leave()
}
if !success {
print("failed to create VM")
}
}
)
}
group.notify(queue: .main) {
self.collectionView?.reloadData()
}
}
}
}
extension BetaClipsViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return viewModels.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModels[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cellType = viewModels[indexPath.section][indexPath.row]
switch cellType {
case .clip(let viewModel):
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ClipsCollectionViewCell.identifier,
for: indexPath)
as? ClipsCollectionViewCell else {
fatalError()
}
cell.delegate = self
cell.configure(with: viewModel)
return cell
}
}
}
extension BetaClipsViewController: ClipsCollectionViewCellDelegate {
func didTapProfile(with model: VideoModel) {
print("profile tapped")
let owner = model.username
DatabaseManager.shared.findUser(username: owner) { [weak self] user in
DispatchQueue.main.async {
guard let user = user else {
return
}
let vc = ProfileViewController(user: user)
self?.navigationController?.pushViewController(vc, animated: true)
}
}
}
func didTapShare(with model: VideoModel) {
print("profile share")
}
func didTapNewClip(with model: VideoModel) {
let vc = RecordViewController()
navigationController?.pushViewController(vc, animated: true)
}
}
extension BetaClipsViewController {
func createViewModel(
model: Clip,
username: String,
completion: #escaping (Bool) -> Void
) {
StorageManager.shared.profilePictureURL(for: username) { [weak self] profilePictureURL in
guard let clipURL = URL(string: model.clipUrlString),
let profilePhotoUrl = profilePictureURL else {
return
}
let clipData: [ClipFeedCellType] = [
.clip(viewModel: VideoModel(username: username,
videoFileURL: model.clipUrlString))
]
self?.viewModels.append(clipData)
completion(true)
}
}
}
I think everything is set up properly in the code so why do no cells show in the collectionView? The view controller shows up as completely blank besides the background color.
I am trying to make an Amazon clone. I am working on the products details page. basically you search, you click the tableviewcell for the item you want and you land on the details page. everything loads except for the collectionview which contains the images. when I remove the stackview everything loads fine but when I embed the view's contents into the stackview the collectionviewcell disappears. the cellForItemAt method doesn't even get called. I don't know why and I could really use some help.
edit: it seems the imageView in the collection cell css unwrapping as nil. I'm not sure why. I connected it to the cell from the storyboard. not sure why it's not working. I turned it into an optional cell.imagioView?.kf.setImage(with: imageUrl) but the collection view loads without the images inside. I finally get the horizontal scrollable action but the images don't load. I'm printing the url for the image when it comes into focus but it cannot be added to the cell. it seems the collection cell is loading but the image can't be added for some reason.
here's the view controller that is supposed to display it
class ProductViewController: UIViewController, UICollectionViewDelegate {
#IBOutlet var stackedView: UIStackView!
#IBOutlet weak var detailedView: UIView!
#IBOutlet var detailPageView: UIScrollView!
#IBOutlet var imagesCarouselCollection: UICollectionView!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var desc: UILabel!
#IBOutlet weak var features: UILabel!
#IBOutlet weak var price: UILabel!
#IBOutlet weak var starsLabel: UILabel!
#IBOutlet weak var noOfReviews: UILabel!
#IBOutlet weak var addToCartBtn: UIButton!
#IBOutlet weak var buyNowBtn: UIButton!
var searchedText: String = ""
var asinForSearch: String = ""
var resultsManager = ResultsManager()
var productDeets: Products?
var imagesArray = Array<String>()
override func viewDidLoad() {
super.viewDidLoad()
resultsManager.detailsDelegate = self
search()
self.setupUI()
loadingIndicator.isAnimating = true
imagesCarouselCollection.delegate = self
imagesCarouselCollection.dataSource = self
imagesCarouselCollection.register(ImageCarouselViewCell.self, forCellWithReuseIdentifier: "ImageCarouselCollectionCell")
imagesCarouselCollection.frame = imagesCarouselCollection.bounds
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.populatePage()
}
}
#IBAction func buyNow(_ sender: Any) {
}
#IBAction func addToCart(_ sender: Any) {
}
func search(){
resultsManager.getDetails(asinForSearch)
}
func createFeaturesLabels(featuresArray: [String]) -> String {
var newFeatureString = ""
for feature in featuresArray {
newFeatureString.append(feature + "\n")
}
return newFeatureString
}
func populatePage(){
if let productResults = self.productDeets {
self.titleLabel.text = productResults.title
self.desc.text = productResults.productDescription
self.imagesArray = productDeets!.images
self.features.text = createFeaturesLabels(featuresArray: productResults.featureBullets)
self.price.text = "$" + String(productResults.price.currentPrice)
self.starsLabel.text = productResults.reviews.rating
self.noOfReviews.text = String(productResults.reviews.totalReviews)
self.loadingIndicator.isAnimating = false
self.stackedView.isHidden = false
}
}
// MARK: - UI Setup for loading icon
private func setupUI() {
if #available(iOS 13.0, *) {
overrideUserInterfaceStyle = .light
}
self.stackedView.isHidden = true
self.detailPageView.backgroundColor = .white
self.detailPageView.addSubview(loadingIndicator)
NSLayoutConstraint.activate([
loadingIndicator.centerXAnchor
.constraint(equalTo: self.view.centerXAnchor),
loadingIndicator.centerYAnchor
.constraint(equalTo: self.view.centerYAnchor),
loadingIndicator.widthAnchor
.constraint(equalToConstant: 50),
loadingIndicator.heightAnchor
.constraint(equalTo: self.loadingIndicator.widthAnchor)
])
}
// MARK: - Properties
let loadingIndicator: ProgressView = {
let progress = ProgressView(colors: [.red, .systemGreen, .systemBlue], lineWidth: 5)
progress.translatesAutoresizingMaskIntoConstraints = false
return progress
}()
}
extension ProductViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width: CGFloat = imagesCarouselCollection.bounds.size.width
let height: CGFloat = imagesCarouselCollection.bounds.size.height
return CGSize(width: width, height: height)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCarouselCollectionCell", for: indexPath) as! ImageCarouselViewCell
let imageUrl = URL(string: imagesArray[indexPath.item])
cell.imagioView.kf.setImage(with: imageUrl)
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print(imagesArray.count)
return imagesArray.count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}
extension ProductViewController: ResultsDetailDelegate {
func updateDetails(_ resultsManager: ResultsManager, products: Products) {
self.productDeets = products
}
}
class ImageCarouselViewCell: UICollectionViewCell {
#IBOutlet weak var imagioView: UIImageView!
}
I found the problem - it was with the layout. It was just as I suspected, half code from tutorials that a wasn't deleted. ended up trying to declare the collectionview and its contents in the storyboard and programmatically. it was fixed by following the layout advice in this tutorial https://www.youtube.com/watch?v=eWGu3hcL3ww
and for anyone else facing this problem here's the updated code.
class ProductViewController: UIViewController, UICollectionViewDelegate {
#IBOutlet var stackedView: UIStackView!
#IBOutlet weak var detailedView: UIView!
#IBOutlet var detailPageView: UIScrollView!
#IBOutlet var imagesCarouselCollection: UICollectionView!
#IBOutlet weak var imagesCarouselCollectionHeight: NSLayoutConstraint!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var desc: UILabel!
#IBOutlet weak var features: UILabel!
#IBOutlet weak var price: UILabel!
#IBOutlet weak var starsLabel: UILabel!
#IBOutlet weak var noOfReviews: UILabel!
#IBOutlet weak var addToCartBtn: UIButton!
#IBOutlet weak var buyNowBtn: UIButton!
var searchedText: String = ""
var asinForSearch: String = ""
var resultsManager = ResultsManager()
var productDeets: Products?
var imagesArray = Array<String>()
override func viewDidLoad() {
super.viewDidLoad()
resultsManager.detailsDelegate = self
search()
self.setupUI()
loadingIndicator.isAnimating = true
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 349, height: 410)
imagesCarouselCollection.collectionViewLayout = layout
layout.scrollDirection = .horizontal
imagesCarouselCollection.register(ImageCollectionViewCell.nib(), forCellWithReuseIdentifier: ImageCollectionViewCell.identifier)
imagesCarouselCollection.frame = imagesCarouselCollection.bounds
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.imagesCarouselCollection.delegate = self
self.imagesCarouselCollection.dataSource = self
self.populatePage()
}
}
#IBAction func buyNow(_ sender: Any) {
}
#IBAction func addToCart(_ sender: Any) {
}
func search(){
resultsManager.getDetails(asinForSearch)
}
func createFeaturesLabels(featuresArray: [String]) -> String {
var newFeatureString = ""
for feature in featuresArray {
newFeatureString.append(feature + "\n")
}
return newFeatureString
}
func populatePage(){
if let productResults = self.productDeets {
self.titleLabel.text = productResults.title
self.desc.text = productResults.productDescription
self.imagesArray = productDeets!.images
self.features.text = createFeaturesLabels(featuresArray: productResults.featureBullets)
self.price.text = "$" + String(productResults.price.currentPrice)
self.starsLabel.text = productResults.reviews.rating + " out of 5 stars"
self.noOfReviews.text = String(productResults.reviews.totalReviews) + " reviews"
self.loadingIndicator.isAnimating = false
self.stackedView.isHidden = false
}
}
// MARK: - UI Setup for loading icon
private func setupUI() {
if #available(iOS 13.0, *) {
overrideUserInterfaceStyle = .light
}
self.stackedView.isHidden = true
self.detailPageView.backgroundColor = .white
self.detailPageView.addSubview(loadingIndicator)
NSLayoutConstraint.activate([
loadingIndicator.centerXAnchor
.constraint(equalTo: self.view.centerXAnchor),
loadingIndicator.centerYAnchor
.constraint(equalTo: self.view.centerYAnchor),
loadingIndicator.widthAnchor
.constraint(equalToConstant: 50),
loadingIndicator.heightAnchor
.constraint(equalTo: self.loadingIndicator.widthAnchor)
])
}
// MARK: - Properties
let loadingIndicator: ProgressView = {
let progress = ProgressView(colors: [.red, .systemGreen, .systemBlue], lineWidth: 5)
progress.translatesAutoresizingMaskIntoConstraints = false
return progress
}()
}
extension ProductViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCollectionViewCell.identifier, for: indexPath) as! ImageCollectionViewCell
let imageUrl = URL(string: imagesArray[indexPath.item])
cell.configure(with: imageUrl!)
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imagesArray.count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}
extension ProductViewController: ResultsDetailDelegate {
func updateDetails(_ resultsManager: ResultsManager, products: Products) {
self.productDeets = products
}
}
I also moved away from just using a CocoaTouch class and created a new one with a xib/nib
class ImageCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var productImageView: UIImageView!
static let identifier = "ImageCollectionViewCell"
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
public func configure(with imageURL: URL) {
productImageView.kf.setImage(with: imageURL)
}
static func nib() -> UINib {
return UINib(nibName: "ImageCollectionViewCell", bundle: nil)
}
}
How can I manually decide the next focused index path for my collection view on tvOS?
My use case is that I have a collection view with dynamic cells that scrolls in all directions, and sometimes when a cell in one section spans across several cells in the next one, I want to be able to manually decide what cell gets focused next.
The default behaviour seems to be that the collection view will select the cell in the middle of the previous one.
For example, the green cell is currently focused. When I navigate down, the collection view wants to focus the red cell, but I want the blue cell to be focused next.
My initial idea was to implement the collectionView(_:shouldUpdateFocusIn:) delegate function, and return false + trigger a focus update, when the collection view selects the "wrong" index path.
However, for some reason, shouldUpdateFocusIn gets called several times when I return false from it and causes a visible lag.
func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
if let nextIndexPath = shouldFocusCell(context.focusHeading, (context.previouslyFocusedIndexPath, context.nextFocusedIndexPath)) {
// The collection view did not select the index path of the cell we want to focus next.
// Save the correct index path and trigger a focus update.
lastFocusChange = (lastFocusChange.next, nextIndexPath)
setNeedsFocusUpdate()
// Using updateFocusIfNeeded() results in the following warning:
// WARNING: Calling updateFocusIfNeeded while a focus update is in progress. This call will be ignored.
return false
}
return true
}
Next idea was to do the same thing in collectionView(_:didUpdateFocusIn:with:), but in this case we only update the focus after it has already moved to the "wrong" cell, so it becomes apparent to the user that the focus moves from the wrong cell to the correct one.
Not ideal either.
I'm using my own subclass of UICollectionViewLayout and UICollectionView, but I don't see anything I can override to be able to manually decide what index path to focus next when navigating up/down/left/right before shouldUpdateFocusIn is called.
Is there any way I can achieve this?
One possibility could be to use collectionView(_ collectionView:, canFocusItemAt:) to let your collectionView know if a given indexPath can receive the focus.
You can find here below a naive implementation of this concept. You will need to adjust the maths on it to your needs.
func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
guard let currentlyFocusedCellLayoutAttributes = collectionView.layoutAttributesForItem(at: focusedIndexPath) else { return false }
guard let cellAtGivenIndexPathLayoutAttributes = collectionView.layoutAttributesForItem(at: indexPath) else { return false }
let currentlyFocusedCellOriginX = currentlyFocusedCellLayoutAttributes.frame.origin.x
let currentlyFocusedCellOriginY = currentlyFocusedCellLayoutAttributes.frame.origin.y
let currentlyFocusedCellWidth = currentlyFocusedCellLayoutAttributes.frame.width
let cellAtGivenIndexPathOriginX = cellAtGivenIndexPathLayoutAttributes.frame.origin.x
let cellAtGivenIndexPathOriginY = cellAtGivenIndexPathLayoutAttributes.frame.origin.y
let cellAtGivenIndexPathWidth = cellAtGivenIndexPathLayoutAttributes.frame.width
let offsetX = collectionView.contentOffset.x
// Scrolling horizontally is always allowed
if currentlyFocusedCellOriginY == cellAtGivenIndexPathOriginY {
return true
}
// Scrolling vertically is only allowed to the first cell (blue cell in the screenshot)
if cellAtGivenIndexPathOriginX <= offsetX {
return true
}
return false
}
We can achieve by using UICollectionView delegate protocol method, pls find code snippt.
class ViewController: UIViewController {
#IBOutlet weak var sideMenuTableView: UITableView!
#IBOutlet weak var collectionView: UICollectionView!
let menueItem = ["Home", "Sports", "Movies", "Listen", "Play", "Game"]
let colors: [UIColor] = [.green, .blue, .purple, .orange, .yellow, .magenta, .brown, .black, .gray, .yellow, .green, .lightGray, .cyan, .magenta, .link, .blue, .yellow, .magenta, .brown, .black, .gray, .yellow, .green, .lightGray, .cyan, .magenta, .link, .blue]
var lastFocusedIndexPath: IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
lastFocusedIndexPath = IndexPath(row: 2, section: 0)
initialConfiguration()
}
override var preferredFocusEnvironments : [UIFocusEnvironment] {
return [collectionView]
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateTableViewContentInset()
}
func updateTableViewContentInset() {
self.sideMenuTableView.contentInset = UIEdgeInsets(top: 0, left: -40, bottom: 0, right: 0)
}
func initialConfiguration() {
sideMenuTableView.register(UINib.init(nibName: "SideMenuTVCell", bundle: nil), forCellReuseIdentifier: "SideMenuTVCell")
sideMenuTableView.delegate = self
sideMenuTableView.dataSource = self
sideMenuTableView.backgroundColor = .yellow
sideMenuTableView.isHidden = false
collectionView.register(UINib.init(nibName: "ColorCVCell", bundle: nil), forCellWithReuseIdentifier: "ColorCVCell")
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .white
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menueItem.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SideMenuTVCell", for: indexPath) as! SideMenuTVCell
cell.configureCell(string: menueItem[indexPath.row])
return cell
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ColorCVCell", for: indexPath) as! ColorCVCell
cell.backgroundColor = colors[indexPath.row]
cell.configureCell(color: colors[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let previousIndexPath = context.previouslyFocusedIndexPath,
let cell = collectionView.cellForItem(at: previousIndexPath) {
cell.contentView.layer.borderWidth = 0.0
cell.contentView.layer.shadowRadius = 0.0
cell.contentView.layer.shadowOpacity = 0
}
if let indexPath = context.nextFocusedIndexPath,
let cell = collectionView.cellForItem(at: indexPath) {
cell.contentView.layer.borderWidth = 8.0
cell.contentView.layer.borderColor = UIColor.black.cgColor
cell.contentView.layer.shadowColor = UIColor.black.cgColor
cell.contentView.layer.shadowRadius = 10.0
cell.contentView.layer.shadowOpacity = 0.9
cell.contentView.layer.shadowOffset = CGSize(width: 0, height: 0)
}
if let indexPath = context.previouslyFocusedIndexPath, let cell = collectionView.cellForItem(at: indexPath) {
UIView.animate(withDuration: 0.3) { () -> Void in
cell.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}
}
if let indexPath = context.nextFocusedIndexPath, let cell = collectionView.cellForItem(at: indexPath) {
UIView.animate(withDuration: 0.3) { () -> Void in
cell.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
if let previouslyFocusedIndexPath = context.previouslyFocusedIndexPath, let cell = collectionView.cellForItem(at: previouslyFocusedIndexPath) {
let collectionViewWidth = collectionView.frame.width
let cellWidth = cell.frame.width
let rowCount = Int(ceil(collectionViewWidth / cellWidth))
let remender = previouslyFocusedIndexPath.row % rowCount
let nextIndex = previouslyFocusedIndexPath.row - remender + rowCount
if let nextFocusedInndexPath = context.nextFocusedIndexPath {
if context.focusHeading == .down {
moveFocus(to: IndexPath(row: nextIndex, section: 0))
return true
}
}
}
return true
}
private func moveFocus(to indexPath: IndexPath) {
lastFocusedIndexPath = indexPath
print(collectionView.indexPathsForVisibleItems)
DispatchQueue.main.async {
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
}
}
func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
return lastFocusedIndexPath
}
}
I have a collectionview with a custom flowlayout and a custom collectionview cell (no storyboards). The custom cell has a CAGradientLayer on a background view. When coming back from suspended state or on traitcollection change this layer is rendered incorrectly (see image:) ) It should be the full width of the cell.
Also when scrolling to off screen items below, the gradient layer isn't rendered at all?
Rotating the device once, or scrolling resolves the issue ...
I'm not sure if this is solvable in the custom cell class or in the collectionview viewcontroller. Reuse issue?
Any help greatly appreciated!
NOTE: Universal app, both ipad and iphone, also split screen compatible.
The cell class
class NormalProjectCell: UICollectionViewCell, SelfConfiguringProjectCell {
//MARK: - Properties
let titleLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .title3), andColor: .label)
let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .caption1), andColor: .secondaryLabel)
let imageView = ProjectImageView(frame: .zero)
var stackView = UIStackView()
var backgroundMaskedView = UIView()
//MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 35
let seperator = Separator(frame: .zero)
stackView = UIStackView(arrangedSubviews: [seperator, titleLabel, lastEditedLabel, imageView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.spacing = 5
stackView.setCustomSpacing(10, after: lastEditedLabel)
stackView.insertSubview(backgroundMaskedView, at: 0)
contentView.addSubview(stackView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()
NSLayoutConstraint.activate([
titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
backgroundMaskedView.translatesAutoresizingMaskIntoConstraints = false
backgroundMaskedView.backgroundColor = .tertiarySystemBackground
backgroundMaskedView.pinToEdges(of: stackView)
let gradientMaskLayer = CAGradientLayer()
gradientMaskLayer.frame = backgroundMaskedView.bounds
gradientMaskLayer.colors = [UIColor.systemPurple.cgColor, UIColor.clear.cgColor]
gradientMaskLayer.locations = [0, 0.4]
backgroundMaskedView.layer.mask = gradientMaskLayer
}
//MARK: - Configure
func configure(with project: ProjectsController.Project) {
titleLabel.text = project.title
lastEditedLabel.text = project.lastEdited.customMediumToString
imageView.image = Bundle.getProjectImage(project: project)
}
}
and the viewcontroller with the collectionView:
class ProjectsViewController: UIViewController {
//MARK: - Types
enum Section: CaseIterable {
case normal
}
//MARK: - Properties
let projectsController = ProjectsController()
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, Project>!
var lastScrollPosition: CGFloat = 0
var isSearching = false
let searchController = UISearchController()
//MARK: - ViewController Methods
override func viewDidLoad() {
super.viewDidLoad()
configureViewController()
configureSearchController()
configureCollectionView()
createDataSource()
updateData(on: projectsController.filteredProjects())
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if isSearching {
isSearching.toggle()
searchController.searchBar.text = ""
searchController.resignFirstResponder()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.searchBar.searchTextField.attributedPlaceholder = NSAttributedString(string: "Title or details text ...",
attributes: [NSAttributedString.Key.foregroundColor: UIColor.secondaryLabel])
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
collectionView.collectionViewLayout = UICollectionView.createFlexibleFlowLayout(in: view)
}
//MARK: - DataSource
func createDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Project>(collectionView: collectionView) { (collectionView, indexPath, project) in
return self.configure(NormalProjectCell.self, with: project, for: indexPath)
}
}
func updateData(on projects: [Project]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Project>()
snapshot.appendSections([Section.normal])
snapshot.appendItems(projects)
//apply() is safe to call from a background queue!
self.dataSource.apply(snapshot, animatingDifferences: true)
}
///Configure any type of cell that conforms to selfConfiguringProjectCell!
func configure<T: SelfConfiguringProjectCell>(_ cellType: T.Type, with project: Project, for indexPath: IndexPath) -> T {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else {
fatalError("Unable to dequeue \(cellType)")
}
cell.configure(with: project)
return cell
}
//MARK: - Actions
#objc func addButtonTapped() {
let project = Project()
let viewController = ProjectDetailsViewController(withProject: project)
viewController.delegate = self
navigationController?.pushViewController(viewController, animated: true)
}
#objc private func tapAndHoldCell(recognizer: UILongPressGestureRecognizer) {
if recognizer.state == .ended {
guard let indexPath = collectionView.indexPathForItem(at: recognizer.location(in: self.collectionView)),
let project = dataSource?.itemIdentifier(for: indexPath) else {
return
}
let viewController = ProjectDetailsViewController(withProject: project)
viewController.delegate = self
navigationController?.pushViewController(viewController, animated: true)
}
}
#objc private func swipeFromRightOnCell(recognizer: UISwipeGestureRecognizer) {
if recognizer.state == .ended {
guard let indexPath = collectionView.indexPathForItem(at: recognizer.location(in: self.collectionView)),
let cell = collectionView.cellForItem(at: indexPath),
let project = dataSource?.itemIdentifier(for: indexPath) else {
return
}
let overlay = ProjectCellDeletionOverlay(frame: CGRect(x: cell.bounds.width, y: 0, width: 0, height: cell.bounds.height))
cell.addSubview(overlay)
UIView.animate(withDuration: 0.70, animations: {
overlay.backgroundColor = UIColor.red.withAlphaComponent(0.60)
overlay.frame = CGRect(x: cell.bounds.width / 2, y: 0, width: cell.bounds.width / 2, height: cell.bounds.height)
}) { _ in
self.presentProjectAlertOnMainThread(withTitle: "Delete this Project?",
andMessage: "Are you sure?\nThis cannot be undone!\nAll associated notes will also be deleted!",
andDismissButtonTitle: "Cancel",
andConfirmButtonTitle: "Delete!",
completion: { success in
if success {
UIView.animate(withDuration: 1.40, animations: {
overlay.frame = CGRect(x: 0, y: 0, width: cell.bounds.width, height: cell.bounds.height)
cell.alpha = 0
}) { _ in
self.delete(project)
overlay.removeFromSuperview()
}
} else {
UIView.animate(withDuration: 1.5, animations: {
overlay.frame = CGRect(x: cell.bounds.width, y: 0, width: 0, height: cell.bounds.height)
overlay.alpha = 0
}) { _ in
overlay.removeFromSuperview()
}
}
})
}
}
}
///Will show an overlay view with help text on the app
#objc private func showHelpView() {
let helpViewController = AppHelpViewController(with: HelpViewDisplayTextFor.projects)
helpViewController.modalTransitionStyle = .flipHorizontal
helpViewController.modalPresentationStyle = .fullScreen
present(helpViewController, animated: true)
}
///Will show a menu with several options
#objc private func showMenu() {
}
//MARK: - UI & Layout
private func configureViewController() {
view.backgroundColor = .systemPurple
title = "Projects"
navigationController?.navigationBar.prefersLargeTitles = false
let menu = UIBarButtonItem(image: ProjectImages.BarButton.menu, style: .plain, target: self, action: #selector(showMenu))
let add = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped))
navigationItem.leftBarButtonItems = [menu, add]
let questionMark = UIBarButtonItem(image: ProjectImages.BarButton.questionmark, style: .plain, target: self, action: #selector(showHelpView))
navigationItem.rightBarButtonItem = questionMark
}
private func configureCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UICollectionView.createFlexibleFlowLayout(in: view))
collectionView.delegate = self
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .clear
view.addSubview(collectionView)
collectionView.register(NormalProjectCell.self, forCellWithReuseIdentifier: NormalProjectCell.reuseIdentifier)
let tapAndHold = UILongPressGestureRecognizer(target: self, action: #selector(tapAndHoldCell))
tapAndHold.minimumPressDuration = 0.3
collectionView.addGestureRecognizer(tapAndHold)
let swipeFromRight = UISwipeGestureRecognizer(target: self, action: #selector(swipeFromRightOnCell) )
swipeFromRight.direction = UISwipeGestureRecognizer.Direction.left
collectionView.addGestureRecognizer(swipeFromRight)
}
private func configureSearchController() {
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
//CollectionView under searchbar fix ???
searchController.extendedLayoutIncludesOpaqueBars = true
// searchController.edgesForExtendedLayout = .top
}
}
//MARK: - Ext CollectionView Delegate
extension ProjectsViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let project = dataSource?.itemIdentifier(for: indexPath) else { return }
ProjectsController.activeProject = project
let loadingView = showLoadingView(for: project)
let viewController = SplitOrFlipContainerController()
UIView.animate(withDuration: 1.5, animations: {
loadingView.alpha = 1
}) { (complete) in
self.dismiss(animated: false) {
self.present(viewController, animated: false)
}
}
}
}
//MARK: - Ext Search Results & Bar
extension ProjectsViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let filter = searchController.searchBar.text, filter.isNotEmpty else {
isSearching = false
updateData(on: projectsController.filteredProjects())
return
}
isSearching = true
updateData(on: projectsController.filteredProjects(with: filter.lowercased()))
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
lastScrollPosition = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if lastScrollPosition < scrollView.contentOffset.y {
navigationItem.hidesSearchBarWhenScrolling = true
} else if lastScrollPosition > scrollView.contentOffset.y {
navigationItem.hidesSearchBarWhenScrolling = false
}
}
}
//MARK: - ProjectHandler
extension ProjectsViewController: ProjectHandler {
internal func save(_ project: Project, withImage image: UIImage?) {
//call save and update the snapshot
projectsController.save(project, withImage: image)
updateData(on: projectsController.filteredProjects())
collectionView.reloadData()
}
internal func delete(_ project: Project) {
//call delete and update the snapshot
projectsController.delete(project)
updateData(on: projectsController.filteredProjects())
}
}
And the flow layout:
extension UICollectionView {
///Flow layout with minimum 2 items across, with padding and spacing
static func createFlexibleFlowLayout(in view: UIView) -> UICollectionViewFlowLayout {
let width = view.bounds.width
let padding: CGFloat
let minimumItemSpacing: CGFloat
let availableWidth: CGFloat
let itemWidth: CGFloat
if view.traitCollection.verticalSizeClass == .compact {
print("//iPhones landscape")
padding = 12
minimumItemSpacing = 12
availableWidth = width - (padding * 2) - (minimumItemSpacing * 3)
itemWidth = availableWidth / 4
} else if view.traitCollection.horizontalSizeClass == .compact && view.traitCollection.verticalSizeClass == .regular {
print("//iPhones portrait")
padding = 12
minimumItemSpacing = 12
availableWidth = width - (padding * 2) - (minimumItemSpacing)
itemWidth = availableWidth / 2
} else {
print("//iPads")
padding = 24
minimumItemSpacing = 24
availableWidth = width - (padding * 2) - (minimumItemSpacing * 3)
itemWidth = availableWidth / 4
}
let flowLayout = UICollectionViewFlowLayout()
flowLayout.sectionInset = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
flowLayout.itemSize = CGSize(width: itemWidth, height: itemWidth + 40)
flowLayout.sectionHeadersPinToVisibleBounds = true
return flowLayout
}
}
Thanks to some help from #nemecek_filip at the HWS forums, I got it solved!
Different approach to the gradient using a custom gradient view!
Hereby the changed and working code:
collectionView cell:
//
// NormalProjectCell.swift
//
import UIKit
class NormalProjectCell: UICollectionViewCell, SelfConfiguringProjectCell {
//MARK: - Properties
let titleLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .title3), andColor: .label)
let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .caption1), andColor: .secondaryLabel)
let imageView = ProjectImageView(frame: .zero)
var stackView = UIStackView()
var backgroundMaskedView = GradientView()
//MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 35
let seperator = Separator(frame: .zero)
stackView = UIStackView(arrangedSubviews: [seperator, titleLabel, lastEditedLabel, imageView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.spacing = 5
stackView.setCustomSpacing(10, after: lastEditedLabel)
stackView.insertSubview(backgroundMaskedView, at: 0)
contentView.addSubview(stackView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()
NSLayoutConstraint.activate([
titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
backgroundMaskedView.translatesAutoresizingMaskIntoConstraints = false
backgroundMaskedView.pinToEdges(of: stackView)
}
//MARK: - Configure
func configure(with project: ProjectsController.Project) {
titleLabel.text = project.title
lastEditedLabel.text = project.lastEdited.customMediumToString
imageView.image = Bundle.getProjectImage(project: project)
}
}
And the GradientView:
//
// GradientView.swift
//
import UIKit
class GradientView: UIView {
var topColor: UIColor = UIColor.tertiarySystemBackground
var bottomColor: UIColor = UIColor.systemPurple
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override func layoutSubviews() {
(layer as! CAGradientLayer).colors = [topColor.cgColor, bottomColor.cgColor]
(layer as! CAGradientLayer).locations = [0.0, 0.40]
}
}
I am developing an application with Swift 4 (Xcode 9.2).
I followed this tutorial. I get my location my location but when I search for a place, the map was hidden: Search a place and it reappear just when I delete all what I write in the search bar as the original one(just My location).
Here is my code :
protocol HandleMapSearch: class {
func dropPinZoomIn(placemark:MKPlacemark)
}
class ViewController: UIViewController {
var selectedPin: MKPlacemark?
var resultSearchController: UISearchController!
let locationManager = CLLocationManager()
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.requestLocation()
let locationSearchTable = storyboard!.instantiateViewController(withIdentifier: "LocationSearchTable") as! LocationSearchTable
resultSearchController = UISearchController(searchResultsController: locationSearchTable)
resultSearchController.searchResultsUpdater = locationSearchTable as UISearchResultsUpdating
let searchBar = resultSearchController!.searchBar
searchBar.sizeToFit()
searchBar.placeholder = "Search for places"
navigationItem.titleView = resultSearchController?.searchBar
resultSearchController.hidesNavigationBarDuringPresentation = false
resultSearchController.dimsBackgroundDuringPresentation = true
definesPresentationContext = true
locationSearchTable.mapView = mapView
locationSearchTable.handleMapSearchDelegate = self
}
#objc func getDirections(){
guard let selectedPin = selectedPin else { return }
let mapItem = MKMapItem(placemark: selectedPin)
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
mapItem.openInMaps(launchOptions: launchOptions)
}
}
extension ViewController : CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("error:: \(error.localizedDescription)")
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
locationManager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if locations.first != nil {
print("location:: (location)")
}
}
}
extension ViewController: HandleMapSearch {
func dropPinZoomIn(placemark: MKPlacemark){
// cache the pin
selectedPin = placemark
// clear existing pins
mapView.removeAnnotations(mapView.annotations)
let annotation = MKPointAnnotation()
annotation.coordinate = placemark.coordinate
annotation.title = placemark.name
if let city = placemark.locality,
let state = placemark.administrativeArea {
annotation.subtitle = "\(city) \(state)"
}
mapView.addAnnotation(annotation)
let span = MKCoordinateSpanMake(0.05, 0.05)
let region = MKCoordinateRegionMake(placemark.coordinate, span)
mapView.setRegion(region, animated: true)
}
}
extension ViewController : MKMapViewDelegate {
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?{
guard !(annotation is MKUserLocation) else { return nil }
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
}
pinView?.pinTintColor = UIColor.orange
pinView?.canShowCallout = true
let smallSquare = CGSize(width: 30, height: 30)
let button = UIButton(frame: CGRect(origin: CGPoint.zero, size: smallSquare))
//button.setBackgroundImage(UIImage(named: "car"), for: .Normal)
button.addTarget(self, action: #selector(ViewController.getDirections), for: .touchUpInside)
pinView?.leftCalloutAccessoryView = button
return pinView
}
}
and this is the LocationSearchTable :
class LocationSearchTable: UITableViewController {
weak var handleMapSearchDelegate: HandleMapSearch?
var matchingItems: [MKMapItem] = []
var mapView: MKMapView?
func parseAddress(selectedItem:MKPlacemark) -> String {
// put a space between "4" and "Melrose Place"
let firstSpace = (selectedItem.subThoroughfare != nil &&
selectedItem.thoroughfare != nil) ? " " : ""
// put a comma between street and city/state
let comma = (selectedItem.subThoroughfare != nil || selectedItem.thoroughfare != nil) &&
(selectedItem.subAdministrativeArea != nil || selectedItem.administrativeArea != nil) ? ", " : ""
// put a space between "Washington" and "DC"
let secondSpace = (selectedItem.subAdministrativeArea != nil &&
selectedItem.administrativeArea != nil) ? " " : ""
let addressLine = String(
format:"%#%#%#%#%#%#%#",
// street number
selectedItem.subThoroughfare ?? "",
firstSpace,
// street name
selectedItem.thoroughfare ?? "",
comma,
// city
selectedItem.locality ?? "",
secondSpace,
// state
selectedItem.administrativeArea ?? ""
)
return addressLine
}
}
extension LocationSearchTable : UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
guard let mapView = mapView,
let searchBarText = searchController.searchBar.text else { return }
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = searchBarText
request.region = mapView.region
let search = MKLocalSearch(request: request)
search.start { response, _ in
guard let response = response else {
return
}
self.matchingItems = response.mapItems
self.tableView.reloadData()
}
}
}
extension LocationSearchTable {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return matchingItems.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
let selectedItem = matchingItems[indexPath.row].placemark
cell.textLabel?.text = selectedItem.name
cell.detailTextLabel?.text = parseAddress(selectedItem: selectedItem)
return cell
}
}
extension LocationSearchTable {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedItem = matchingItems[indexPath.row].placemark
handleMapSearchDelegate?.dropPinZoomIn(placemark: selectedItem)
dismiss(animated: true, completion: nil)
}
}
Please I need your help to solve this issue.
You are using the wrong delegate func updateSearchResultsForSearchController. Use the following code.
extension LocationSearchTable : UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let mapView = mapView,
let searchBarText = searchController.searchBar.text else { return }
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = searchBarText
request.region = mapView.region
let search = MKLocalSearch(request: request)
search.start { response, _ in
guard let response = response else {
return
}
self.matchingItems = response.mapItems
self.tableView.reloadData()
}
}
}
Also, if you like to search the city only, you can use locality only. See the example below.Otherwise, keep your code intact.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
let selectedItem = matchingItems[indexPath.row].placemark
cell.textLabel?.text = selectedItem.locality
cell.detailTextLabel?.text = "" //parseAddress(selectedItem: selectedItem)
return cell
}
[EDIT 1]
Another deprecated delegate needs to be changed for getting direction:
extension ViewController : MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
guard !(annotation is MKUserLocation) else { return nil }
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
}
pinView?.pinTintColor = UIColor.orange
pinView?.canShowCallout = true
let smallSquare = CGSize(width: 30, height: 30)
let button = UIButton(frame: CGRect(origin: CGPoint.zero, size: smallSquare))
button.setBackgroundImage(UIImage(named: "car"), for: .normal)
button.addTarget(self, action: #selector(ViewController.getDirections), for: .touchUpInside)
pinView?.leftCalloutAccessoryView = button
return pinView
}
}