Why is my UICollectionView not loading cells? - uicollectionview

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.

Related

Make NSCollectionLayoutItem heights as tall as the NSCollectionLayoutGroup when using estimated heights in a UICollectionViewLayout grid

I have a UICollectionViewLayout grid with three columns. Each item in the column has a cell full of text. I would like all the columns to be the same height as the tallest item in the group. Using UICollectionViewCompositionalLayout I'm having a hard time getting the desired results.
Any ideas on how to achieve this? All the code is below. The createLayout function is the most relevant. Please note I am using LoremSwiftum to generate the random text.
Current behavior
Expected behavior:
I was able to mock this expected behavior by changing the NSCollectionLayoutSize to be an absolute value of 500. This is not what I want. Each cell should estimate its size, and use the largest value of the group.
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(500))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(500))
Entire Code:
import UIKit
import LoremSwiftum
class ViewController: UIViewController {
enum Section {
case main
}
var dataSource: UICollectionViewDiffableDataSource<Section, Int>! = nil
var collectionView: UICollectionView! = nil
let sectionBackgroundDecorationElementKind = "section-background-element-kind"
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Two-Column Grid"
configureHierarchy()
configureDataSource()
}
/// - Tag: TwoColumn
func createLayout() -> UICollectionViewLayout {
let spacing = CGFloat(10)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
func configureHierarchy() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground
view.addSubview(collectionView)
}
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<TextCell, Int> { (cell, indexPath, identifier) in
// Populate the cell with our item description.
cell.label.text = Lorem.sentences(Bool.random() ? 1 : 3)
}
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
// Return the cell.
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
}
// initial data
var snapshot = NSDiffableDataSourceSnapshot<Section, Int>()
snapshot.appendSections([.main])
snapshot.appendItems(Array(0..<94))
dataSource.apply(snapshot, animatingDifferences: false)
}
}
class TextCell: UICollectionViewCell {
let label = UILabel()
let bottomLabel = UILabel()
let container = UIView()
static let reuseIdentifier = "text-cell-reuse-identifier"
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("not implemented")
}
func configure() {
label.numberOfLines = 0
label.adjustsFontForContentSizeCategory = true
label.font = UIFont.preferredFont(forTextStyle: .title1)
label.backgroundColor = .systemPurple
label.textColor = .white
bottomLabel.numberOfLines = 0
bottomLabel.adjustsFontForContentSizeCategory = true
bottomLabel.font = UIFont.preferredFont(forTextStyle: .title1)
bottomLabel.text = "Bottom of cell"
bottomLabel.backgroundColor = .systemRed
bottomLabel.textColor = .white
let stack = UIStackView(arrangedSubviews: [label, bottomLabel])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.distribution = .equalSpacing
stack.spacing = 20
contentView.addSubview(stack)
backgroundColor = .systemBlue.withAlphaComponent(0.75)
let inset = CGFloat(10)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: inset),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -inset),
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: inset),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -inset),
])
}
}
I ended up having to create a UICollectionViewCompositionalLayout subclass, but the solution works really well.
Using the new subclass, keep a weak reference to the layout in your cell so you can use it in preferredLayoutAttributesFitting.
class EqualHeightsUICollectionViewCompositionalLayout: UICollectionViewCompositionalLayout {
private var heights = [Int: [IndexPath: CGFloat]]()
private var largests = [Int: CGFloat]()
private let columns: Int
init(section: NSCollectionLayoutSection, columns: Int) {
self.columns = columns
super.init(section: section)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
if #unavailable(iOS 15) {
if let attributes = attributes {
for attribute in attributes {
updateLayoutAttributesHeight(layoutAttributes: attribute)
}
}
}
return attributes
}
override func invalidateLayout() {
super.invalidateLayout()
heights.removeAll(keepingCapacity: true)
largests.removeAll(keepingCapacity: true)
}
func updateLayoutAttributesHeight(layoutAttributes: UICollectionViewLayoutAttributes) {
let height = layoutAttributes.frame.height
let indexPath = layoutAttributes.indexPath
let row = indexPath.item / columns
heights[row]?[indexPath] = height
largests[row] = max(largests[row] ?? 0, height)
let size = CGSize(width: layoutAttributes.frame.width,
height: largests[row] ?? 0)
layoutAttributes.frame = .init(origin: layoutAttributes.frame.origin, size: size)
}
}
class TextCell: UICollectionViewCell {
let label = UILabel()
weak var layout: EqualHeightsUICollectionViewCompositionalLayout?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .systemBlue.withAlphaComponent(0.75)
configure()
}
required init?(coder: NSCoder) {
fatalError("not implemented")
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let attribute = super.preferredLayoutAttributesFitting(layoutAttributes)
layout?.updateLayoutAttributesHeight(layoutAttributes: attribute)
return attribute
}
private func configure() {
let bottomLabel = UILabel()
label.numberOfLines = 0
label.font = UIFont.preferredFont(forTextStyle: .title1)
label.backgroundColor = .systemPurple
label.textColor = .white
label.setContentHuggingPriority(.defaultHigh, for: .vertical)
bottomLabel.numberOfLines = 0
bottomLabel.font = UIFont.preferredFont(forTextStyle: .title1)
bottomLabel.text = "Bottom of cell"
bottomLabel.backgroundColor = .systemRed
bottomLabel.textColor = .white
bottomLabel.setContentHuggingPriority(.defaultLow, for: .vertical)
let stackView = UIStackView(arrangedSubviews: [label, bottomLabel])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 20
contentView.addSubview(stackView)
let padding: CGFloat = 15
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding),
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
])
}
}

CollectionView FlowLayout custom cell rendering issues

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]
}
}

How to search for location and display results with MapKit Swift 4

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
}
}

tvos: UITextView focus appearance like movies App

I am building a tvos app and i want the UITextView to behave similarly like in tvos Movies app. I am specially interested in the focused appearence. Please have a look ate these two pictures.
Currently i am just adding background color to the textview when it is focused but how i can achieve this focused appearance in the attached images. here is my small code
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
if context.previouslyFocusedView == lessonDescriptionTxt {
coordinator.addCoordinatedAnimations({ () -> Void in
self.lessonDescriptionTxt.layer.backgroundColor = UIColor.clearColor().CGColor
}, completion: nil)
}
if context.nextFocusedView == lessonDescriptionTxt {
coordinator.addCoordinatedAnimations({ () -> Void in
self.lessonDescriptionTxt.layer.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.2).CGColor
}, completion: nil)
}
}
Also if someone can suggest how i can achieve this MORE feature in the textView when there is more text. I also read this question Make UILabel focusable and tappable (tvOS) but that does not do the job for me.
The "MORE" text appears only when the description can't fit in the space that's available. You should be able to measure the amount of space needed by text by using UIKit's -[NSAttributedString boundingRectWithSize:options:context:] method.
Implementing custom FocusableTextView class worked for me:
class FocusableTextView: UITextView {
let suffixWithMore = " ... MORE"
weak var tapDelegate: FocusableTextViewDelegate?
var currentText = ""
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
isScrollEnabled = true // must be set to true for 'stringThatFitsOnScreen' function to work
isUserInteractionEnabled = true
isSelectable = true
textAlignment = .justified
self.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
tap.allowedPressTypes = [NSNumber(value: UIPress.PressType.select.rawValue)]
self.addGestureRecognizer(tap)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// use setText(_:) function instead of assigning directly to text variable
func setText(_ txt: String) {
currentText = txt
let stringThatFits = stringThatFitsOnScreen(originalString: txt) ?? txt
if txt <= stringThatFits {
self.text = txt
} else {
let newString = makeStringWithMORESuffix(from: stringThatFits)
self.text = newString
}
}
func makeStringWithMORESuffix(from txt: String) -> String {
let newNSRange = NSMakeRange(0, txt.count - suffixWithMore.count)
let stringRange = Range(newNSRange,in: txt)!
let subString = String(txt[stringRange])
return subString + suffixWithMore
}
func stringThatFitsOnScreen(originalString: String) -> String? {
// the visible rect area the text will fit into
let userWidth = self.bounds.size.width - self.textContainerInset.right - self.textContainerInset.left
let userHeight = self.bounds.size.height - self.textContainerInset.top - self.textContainerInset.bottom
let rect = CGRect(x: 0, y: 0, width: userWidth, height: userHeight)
// we need a new UITextView object to calculate the glyphRange. This is in addition to
// the UITextView that actually shows the text (probably a IBOutlet)
let tempTextView = UITextView(frame: self.bounds)
tempTextView.font = self.font
tempTextView.text = originalString
// get the layout manager and use it to layout the text
let layoutManager = tempTextView.layoutManager
layoutManager.ensureLayout(for: tempTextView.textContainer)
// get the range of text that fits in visible rect
let rangeThatFits = layoutManager.glyphRange(forBoundingRect: rect, in: tempTextView.textContainer)
// convert from NSRange to Range
guard let stringRange = Range(rangeThatFits, in: originalString) else {
return nil
}
// return the text that fits
let subString = originalString[stringRange]
return String(subString)
}
#objc func tapped(_ gesture: UITapGestureRecognizer) {
print("user selected TextView!")
tapDelegate?.userSelectedText(currentText)
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if (context.nextFocusedView == self) {
backgroundColor = .white
textColor = .black
}
else {
backgroundColor = .clear
textColor = .white
}
}
}
protocol FocusableTextViewDelegate: AnyObject {
func userSelectedText(_ txt: String)
}
When user taps the text View, you can present Alert with full text likewise:
extension YourViewController: FocusableTextViewDelegate{
func userSelectedText(_ txt: String) {
let alert = UIAlertController(title: "", message: txt, preferredStyle: .alert)
let action = UIAlertAction( title: nil, style: .cancel) {_ in }
alert.addAction(action)
self.present(alert, animated: true)
}
}
Usage:
create FocusableTextView programmatically:
assign constraints programmatically to textView (or use frame)
set text to FocusableTextView with setText(_:) method
assing your UIViewController to be the tapDelegate
override func viewDidLoad() {
super.viewDidLoad()
let textView = FocusableTextView(frame: CGRect(x: 0, y: 0,
width: 500,
height: 300),
textContainer: nil)
textView.setText("Your long text..")
textView.tapDelegate = self
}

custom search bar using UISearchController in iOS 8

I have a custom search bar class, and I used interface builder to insert a search bar as an instance of this custom class. How do I use this search bar is search bar for the UISearchController? Its searchBar property is read-only so I can't assign my search bar to it.
You have to set your already initialized custom search bar to be returned in the overriden search bar getter for you custom UISearchController class and setup its properties after the searchcontroller init, this way all the UISearchController functionality are retained:
public class DSearchController: UISearchController {
private var customSearchBar = DSearchBar()
override public var searchBar: UISearchBar {
get {
return customSearchBar
}
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
public init(searchResultsController: UIViewController?,
searchResultsUpdater: UISearchResultsUpdating?,
delegate: UISearchControllerDelegate?,
dimsBackgroundDuringPresentation: Bool,
hidesNavigationBarDuringPresentation: Bool,
searchBarDelegate: UISearchBarDelegate?,
searchBarFrame: CGRect?,
searchBarStyle: UISearchBarStyle,
searchBarPlaceHolder: String,
searchBarFont: UIFont?,
searchBarTextColor: UIColor?,
searchBarBarTintColor: UIColor?, // Bar background
searchBarTintColor: UIColor) { // Cursor and bottom line
super.init(searchResultsController: searchResultsController)
self.searchResultsUpdater = searchResultsUpdater
self.delegate = delegate
self.dimsBackgroundDuringPresentation = dimsBackgroundDuringPresentation
self.hidesNavigationBarDuringPresentation = hidesNavigationBarDuringPresentation
customSearchBar.setUp(searchBarDelegate,
frame: searchBarFrame,
barStyle: searchBarStyle,
placeholder: searchBarPlaceHolder,
font: searchBarFont,
textColor: searchBarTextColor,
barTintColor: searchBarBarTintColor,
tintColor: searchBarTintColor)
}
}
And this is my custom searchBar:
public class DSearchBar: UISearchBar {
var preferredFont: UIFont?
var preferredTextColor: UIColor?
init(){
super.init(frame: CGRect.zero)
}
func setUp(delegate: UISearchBarDelegate?,
frame: CGRect?,
barStyle: UISearchBarStyle,
placeholder: String,
font: UIFont?,
textColor: UIColor?,
barTintColor: UIColor?,
tintColor: UIColor?) {
self.delegate = delegate
self.frame = frame ?? self.frame
self.searchBarStyle = searchBarStyle
self.placeholder = placeholder
self.preferredFont = font
self.preferredTextColor = textColor
self.barTintColor = barTintColor ?? self.barTintColor
self.tintColor = tintColor ?? self.tintColor
self.bottomLineColor = tintColor ?? UIColor.clearColor()
sizeToFit()
// translucent = false
// showsBookmarkButton = false
// showsCancelButton = true
// setShowsCancelButton(false, animated: false)
// customSearchBar.backgroundImage = UIImage()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let bottomLine = CAShapeLayer()
var bottomLineColor = UIColor.clearColor()
override public func layoutSubviews() {
super.layoutSubviews()
for view in subviews {
if let searchField = view as? UITextField { setSearchFieldAppearance(searchField); break }
else {
for sView in view.subviews {
if let searchField = sView as? UITextField { setSearchFieldAppearance(searchField); break }
}
}
}
bottomLine.path = UIBezierPath(rect: CGRectMake(0.0, frame.size.height - 1, frame.size.width, 1.0)).CGPath
bottomLine.fillColor = bottomLineColor.CGColor
layer.addSublayer(bottomLine)
}
func setSearchFieldAppearance(searchField: UITextField) {
searchField.frame = CGRectMake(5.0, 5.0, frame.size.width - 10.0, frame.size.height - 10.0)
searchField.font = preferredFont ?? searchField.font
searchField.textColor = preferredTextColor ?? searchField.textColor
//searchField.backgroundColor = UIColor.clearColor()
//backgroundImage = UIImage()
}
}
Init example:
searchController = DSearchController(searchResultsController: ls,
searchResultsUpdater: self,
delegate: self,
dimsBackgroundDuringPresentation: true,
hidesNavigationBarDuringPresentation: true,
searchBarDelegate: ls,
searchBarFrame: CGRectMake(0.0, 0.0, SCREEN_WIDTH, 44.0),
searchBarStyle: .Minimal,
searchBarPlaceHolder: NSLocalizedString("Search a location...", comment: ""),
searchBarFont: nil,
searchBarTextColor: nil,
searchBarBarTintColor: UIColor.whiteColor(),
searchBarTintColor: iconsColor)
searchController.searchBar.keyboardAppearance = .Dark
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar