Swift 5 - why does my collectionview disappear when embedded in a stackview? - uicollectionview

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

Related

How to manually select next focused index path in collection view

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

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

UICollectionView drag and drop while resizing cell

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

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

Why can't I use write(toFile: ) property on a string?

I'm following a tutorial that was written in some earlier version of Swift, that teaches me how to read/write a .txt file in Swift3. Xcode has been doing a good job so far of letting me know when I'm using old syntax, and changing it for me to the latest syntax. However, I'm coming across something that works in an older version of Swift, but not the current one.
class ViewController: UIViewController {
// MARK: Properties
#IBOutlet weak var monthToEditTextField: UITextField!
#IBOutlet weak var bedTimeTextField: UITextField!
#IBOutlet weak var wakeTimeTextField: UITextField!
#IBOutlet weak var theLabel: UILabel!
#IBAction func saveButton(_ sender: UIButton)
{
var theMonth = monthToEditTextField.text
var bedTime = bedTimeTextField.text
var wakeTime = wakeTimeTextField.text
var stringForTXTFile = "The user's info is: \(theMonth), \(bedTime), \(wakeTime)"
let fileManager = FileManager.default
if (!fileManager.fileExists(atPath: filePath))
{
var writeError: NSError?
let fileToBeWritten = stringForTXTFile.write(toFile: // This is where the problem is
}
}
When I type
stringForTXTFile.write
I get this error box
What do I need to do in order to use the "write" property?
Write to file doesn't return Bool anymore in Swift3. It throws, so just delete let fileToBeWritten = and use do try catch error handling. Any code that needs to be run if the operation was successful needs to be placed below that inside the do try curly brackets. You can also use guard to unwrap your textfield optional strings. Try like this:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var monthToEditTextField: UITextField!
#IBOutlet weak var bedTimeTextField: UITextField!
#IBOutlet weak var wakeTimeTextField: UITextField!
#IBOutlet weak var theLabel: UILabel!
let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("textFile.txt")
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func save(_ sender: UIButton) {
guard
let theMonth = monthToEditTextField.text,
let bedTime = bedTimeTextField.text,
let wakeTime = wakeTimeTextField.text
else { return }
let stringForTXTFile = "The user's info is: \(theMonth), \(bedTime), \(wakeTime)"
do {
try stringForTXTFile.write(toFile: fileURL.path, atomically: true, encoding: .utf8)
// place code to be executed here if write was successful
} catch {
print(error.localizedDescription)
}
}
#IBAction func load(_ sender: UIButton) {
do {
theLabel.text = try String(contentsOf: fileURL)
} catch {
print(error.localizedDescription)
}
}
}