How to present a UICollectionView in SwiftUI with UIViewControllerRepresentable - swift

Firstly, I know there are options for using SwiftUI Lists etc... to get similar effects. But I need the automatic scrolling capabilities of a UICollectionView so I'd really like to just implement an "old school" version. I don't even want the compositional layout version ideally.
My current code looks like this:
import SwiftUI
struct CollectionView: UIViewControllerRepresentable {
private var isActive: Binding<Bool>
private let viewController = UIViewController()
private let collectionController: UICollectionView
init(_ isActive: Binding<Bool>) {
self.isActive = isActive
self.collectionController = UICollectionView(frame: CGRect(x: 0, y: 0, width: 100, height: 200), collectionViewLayout: UICollectionViewFlowLayout())
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CollectionView>) -> UIViewController {
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CollectionView>) {
if self.isActive.wrappedValue && collectionController.delegate == nil { // to not show twice
collectionController.delegate = context.coordinator
collectionController.dataSource = context.coordinator
}
}
func makeCoordinator() -> Coordintor {
return Coordintor(owner: self)
}
final class Coordintor: NSObject, UICollectionViewDelegate, UICollectionViewDataSource {
weak var viewController:UIViewController?
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
var cellId = "Cell"
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath)
return cell
}
// works as delegate
let owner: CollectionView
init(owner: CollectionView) {
self.owner = owner
}
}
}
Unfortunately, all I get is a blank screen in the preview. For now if I can just display a big selection of red squares which I can scroll through and auto scroll to the bottom onAppear, that would be ideal.
Thanks!

Here is minimal runnable demo. (Note: Cell have to be registered if all is done programmatically)
class MyCell: UICollectionViewCell {
}
struct CollectionView: UIViewRepresentable {
func makeUIView(context: Context) -> UICollectionView {
let view = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
view.backgroundColor = UIColor.clear
view.dataSource = context.coordinator
view.register(MyCell.self, forCellWithReuseIdentifier: "myCell")
return view
}
func updateUIView(_ uiView: UICollectionView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! MyCell
cell.backgroundColor = UIColor.red
return cell
}
}
}
struct TestUICollectionView_Previews: PreviewProvider {
static var previews: some View {
CollectionView()
}
}

Related

UIViewRepresentable UICollectionView How do you scroll to a certain point when view appears?

I have the following code, but I am not sure where to place it in my UIViewRepresntable. Any suggestions?
let scrollTo = IndexPath(row: 25, section: 0)
view.scrollToItem(at: scrollTo, at: .top, animated: false)
struct CollectionViewRepresentable: UIViewRepresentable {
#StateObject var vm = CollectionViewModel()
let view = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewCompositionalLayout.list(using: UICollectionLayoutListConfiguration(appearance: .plain)))
func makeUIView(context: Context) -> UICollectionView {
view.backgroundColor = UIColor.clear
view.dataSource = context.coordinator
view.delegate = context.coordinator
view.register(ListCell.self, forCellWithReuseIdentifier: "listCell")
return view
}
func updateUIView(_ uiView: UICollectionView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICollectionViewDelegate, UICollectionViewDataSource {
private var parent: CollectionViewRepresentable
init(_ collectionViewRepresentable: CollectionViewRepresentable) {
self.parent = collectionViewRepresentable
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return parent.vm.items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as! ListCell
let item = parent.vm.items[indexPath.row]
cell.item = item
return cell
}
}
}
I need to scroll to that position when the view appears.
You could use ViewControllerRepresentable and take advantage of the fact that view controllers can override the viewWillAppear/viewDidAppear methods where you can write the code that scrolls.
For this, subclass UICollectionViewController, and move all the collection view related logic there:
class MyCollectionViewController: UICollectionViewController {
var vm = CollectionViewModel()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = UIColor.clear
collectionView.register(ListCell.self, forCellWithReuseIdentifier: "listCell")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let scrollTo = IndexPath(row: 25, section: 0)
collectionView.scrollToItem(at: scrollTo, at: .top, animated: false)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return vm.items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as! ListCell
let item = vm.items[indexPath.row]
cell.item = item
return cell
}
}
With the above in mind, the SwiftUI code simplifies to something like this:
struct CollectionViewRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MyCollectionViewController {
.init(collectionViewLayout: UICollectionViewFlowLayout())
}
func updateUIViewController(_ vc: MyCollectionViewController, context: Context) {
}
}
A scenario is not completely clear, but it is possible to use external State and a Binding inside representable, changing with calls updateUIView.
So a possible solution is
#State private var scrollTo = IndexPath(row: 25, section: 0) // << in parent !!
// ...
struct CollectionViewRepresentable: UIViewRepresentable {
#Binding var scrollTo: IndexPath // << 1) !!
#StateObject private var vm = CollectionViewModel()
func makeUIView(context: Context) -> UICollectionView {
let view = UICollectionView(...) // << move creation here !!
// ... other code
return view
}
func updateUIView(_ uiView: UICollectionView, context: Context) {
// called at start and when binding updated
uiView.scrollToItem(at: scrollTo, at: .top, animated: false) // << 2) !!
}
// ... other code
}

Cant display image in UICollectionView cell

I need to display images in collection view cells but when I'm trying to do that I'm getting 10 empty cells and I don't know where im making mistakes
Here is my code of ViewController
class NewGalleryViewController: UIViewController {
var presenter: ViewToPresenterPhotoProtocol?
var builder: GalleryRequestBuilder?
#IBOutlet var collectionView: UICollectionView!
let reuseIdentifier = "customCVCell"
#objc func refresh() {
presenter?.refresh()
}
override func viewDidLoad() {
super.viewDidLoad()
self.setupPresenterIfNeed()
presenter?.viewDidLoad()
// Do any additional setup after loading the view.
}
func setupPresenterIfNeed() {
self.collectionView.backgroundColor = UIColor.white
if self.presenter == nil {
let presenter = GalleryPresenter()
presenter.view = self
self.presenter = presenter
self.builder = GalleryRequestBuilder()
}
}
}
extension NewGalleryViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.presenter?.photos.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PhotoCollectionViewCell
KFImage.url(builder?.createImageUrl(name: (presenter?.photos[indexPath.item].name)!))
.onSuccess { result in
cell.imageView.image = result.image
}
return cell
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 180, height: 128)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20.0
}
// MARK: - UICollectionViewDelegate protocol
private func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// handle tap events
print("You selected cell #\(indexPath.item)!")
}
}
extension NewGalleryViewController: PresenterToViewPhotoProtocol{
func onFetchPhotoSuccess() {
self.collectionView.reloadData()
self.collectionView!.collectionViewLayout.invalidateLayout()
self.collectionView!.layoutSubviews()
self.collectionView.refreshControl?.endRefreshing()
}
func onFetchPhotoFailure(error: String) {
print("View receives the response from Presenter with error: \(error)")
self.collectionView.refreshControl?.endRefreshing()
}
}
And Here is the code of cell
class PhotoCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
I've checked the link I'm making request to and it works. So problem is not in link. Maybe I should reload items after getting images?
You should set your UICollectionView delegate and data source once the view is loaded:
override func viewDidLoad() {
super.viewDidLoad()
// Add this lines
collectionView.delegate = self
collectionView.dataSource = self
self.setupPresenterIfNeed()
presenter?.viewDidLoad()
}

How can I update a UICollectionView from another class?

I am trying this for a couple of days now and haven't achieved anything yet. What I am trying to do is when a user picks a User item form the ViewController class, I want to save it in Realm and show it in the CollectionView of the SavedInterestsViewController class. I use a delegate pattern as suggested in this post How to access and refresh a UITableView from another class in Swift, but unfortunately I still receive nil, I guess because the GC removed the collectionView outlet already right? (please correct me if I misunderstood it). However, how can I get this to work by using a delegate pattern? Here is my code, this is the class where the user Picks a new User-item:
protocol ViewControllerDelegate {
func didUpdate(sender: ViewController)
}
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var collectionView: UICollectionView!
var delegate: ViewControllerDelegate?
let numberOfTweets = 5
let realm = try! Realm()
var image = UIImage()
var imageArray: [String] = []
var userArray: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UINib(nibName: "CollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "CollectionCell")
searchBar.delegate = self
}
}
extension ViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
print("One second...")
let functionClass = NetworkingFunctions()
var loopCount = 0
functionClass.getWikipediaAssumptions(for: searchBar.text!) { [self] (articleArray) in
self.userArray.removeAll()
for x in articleArray {
functionClass.performWikipediaSearch(with: x, language: WikipediaLanguage("en")) { (user) in
self.userArray.append(user)
collectionView.reloadData()
loopCount += 1
}
}
}
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return userArray.count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionCell", for: indexPath) as! CollectionViewCell
let image = UIImage.init(data: userArray[indexPath.item].image as Data)
cell.userImage.image = image
cell.nameLabel.text = userArray[indexPath.item].name
cell.userImage.layer.borderColor = image?.averageColor?.cgColor
if userArray[indexPath.item].checked == false {
cell.checkmark.isHidden = true
} else {
cell.checkmark.isHidden = false
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if userArray[indexPath.item].checked == false {
userArray[indexPath.item].checked = true
collectionView.reloadData()
let newUser = User()
newUser.image = userArray[indexPath.item].image
newUser.name = userArray[indexPath.item].name
newUser.checked = true
try! realm.write {
realm.add(newUser)
}
self.delegate = SavedInterestsViewController()
self.delegate?.didUpdate(sender: self)
}
else {
userArray[indexPath.item].checked = false
collectionView.reloadData()
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = UIScreen.main.bounds.width/3 - 10
let height = width
return CGSize(width: width, height: height)
}
}
class User: Object {
#objc dynamic var image: NSData = NSData()
#objc dynamic var name: String = ""
#objc dynamic var checked: Bool = false
}
... and this is the class where I want to show the selected item, after the User clicked on the 'Back' Button of the navigation controller of the ViewController class:
class SavedInterestsViewController: UIViewController, ViewControllerDelegate {
func didUpdate(sender: ViewController) {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
#IBOutlet weak var addButton: UIBarButtonItem!
#IBOutlet weak var collectionView: UICollectionView!
let realm = try! Realm()
var userArray: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UINib(nibName: "CollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "CollectionCell")
fetchDataFromRealm()
}
#IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
performSegue(withIdentifier: "SavedToNew", sender: self)
}
func fetchDataFromRealm() {
userArray.append(contentsOf: realm.objects(User.self))
collectionView.reloadData()
}
}
extension SavedInterestsViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return userArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionCell", for: indexPath) as! CollectionViewCell
let image = UIImage.init(data: userArray[indexPath.item].image as Data)
cell.userImage.image = image
cell.nameLabel.text = userArray[indexPath.item].name
cell.userImage.layer.borderColor = image?.averageColor?.cgColor
if userArray[indexPath.item].checked == false {
cell.checkmark.isHidden = true
} else {
cell.checkmark.isHidden = false
}
return cell
}
}
extension SavedInterestsViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = UIScreen.main.bounds.width/3 - 10
let height = width
return CGSize(width: width, height: height)
}
}
in your code you have lines
self.delegate = SavedInterestsViewController()
self.delegate?.didUpdate(sender: self)
but it do mostly nothing - you set the delegate to newly created class, didUpdate it - but I don't see any use of it - you don't present it in any way.
If I understand you right - you have SavedInterestsViewController, from it - you open ViewController, do something and when back to SavedInterestsViewController. (I can be wrong with your flow - correct me if so)
In this flow - you have delegate property in ViewController, but it must be of type SavedInterestsViewController. And you have to set it to SavedInterestsViewController when you open ViewController from it. And later in ViewController you have to call didUpdate method of delegate.

My collection view is nil when implicit unwrapping?

I use a UITabBarController to manage the large middle button on the UITabBar. Here is a part of it.
class TabBarController: UITabBarController, UITabBarControllerDelegate, ContentChangedDelegate {
override func viewDidLoad() {
super.viewDidLoad()
setupMiddleButton()
}
func setupMiddleButton() {
let tabBarHeight = tabBar.frame.size.height
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: tabBarHeight*1.5, height: tabBarHeight*1.5))
var menuButtonFrame = menuButton.frame
menuButtonFrame.origin.y = view.bounds.height - menuButtonFrame.height/2 - tabBarHeight - 8
menuButtonFrame.origin.x = view.bounds.width/2 - menuButtonFrame.size.width/2
menuButton.frame = menuButtonFrame
menuButton.backgroundColor = UIColor.red
menuButton.layer.cornerRadius = menuButtonFrame.height/2
view.addSubview(menuButton)
let largeConfiguration = UIImage.SymbolConfiguration(scale: .large)
let addIcon = UIImage(systemName: "plus", withConfiguration: largeConfiguration)
menuButton.setImage((addIcon), for: .normal)
menuButton.addTarget(self, action: #selector(menuButtonAction(sender:)), for: .touchUpInside)
view.layoutIfNeeded()
}
#objc private func menuButtonAction(sender: UIButton) {
AddViewController.delegate = self
performSegue(withIdentifier: "addEventSegue", sender: self)
}
}
and I conform another class LifeViewController(I used the class the specify the collecitonView and searchbar and other items in it) to tabBarController but the lifeCollectionView is always nil when implicitly unwrap, Could any one help me, Thank you so much!
class LifeViewController: TabBarController {
let realm = try! Realm()
var reminders : Results<Memorandum>!
let tabBarView = TabBarController()
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet var lifeCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.fetchData()
lifeCollectionView.delegate = self
lifeCollectionView.dataSource = self
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
self.fetchData()
}
override func reloadCollection() {
if let life = self.lifeCollectionView {
life.reloadData()
} else {
print("life found nil")
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
performSegue(withIdentifier: "editing", sender: self)
}
func fetchData() {
reminders = realm.objects(Memorandum.self)
}
}
//MARK: - CollectionView
extension LifeViewController {
override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: lifeCollectionView.frame.width/2.1, height: lifeCollectionView.frame.width/2.1)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return reminders.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let reminder = reminders[indexPath.item]
let lifeCell = lifeCollectionView.dequeueReusableCell(withReuseIdentifier: "lifeCell", for: indexPath) as! ReminderCell
lifeCell.configureCell(date: reminder.dateReminder, importance: UIColor(hex: reminder.color)!, name: reminder.title, depict: reminder.depiction)
lifeCell.layer.cornerRadius = 12
return lifeCell
}
}
But when I change back to conform the LifeViewController to UIViewController, and other collection view protocols the IBOutlet, delegate and dataSource work again? Why is that?

Swift: NSCollectionViewItem not showing in NSViewController

I am building an app with NSCollectionView and behaviour of NSCollectionView is so strange, sometimes it makes cell's visible and sometimes are not.
#IBOutlet weak var resourceCollectionView: NSCollectionView!
private func prepareCollectionView() {
let flowLayoutForEvent = NSCollectionViewFlowLayout()
flowLayoutForEvent.scrollDirection = .vertical
flowLayoutForEvent.minimumInteritemSpacing = 100
resourceCollectionView.collectionViewLayout = flowLayoutForEvent
resourceCollectionView.delegate = self
resourceCollectionView.dataSource = self
resourceCollectionView.isSelectable = true
resourceCollectionView.backgroundColors = [.clear]
resourceCollectionView.register(SimpleCell.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "SimpleCell"))
}
Then applied the function
public override func viewDidLoad() {
super.viewDidLoad()
prepareCollectionView()
}
Here is extension function for NSCollectionView
extension DashboardController : NSCollectionViewDelegateFlowLayout, NSCollectionViewDataSource {
// 1
public func numberOfSections(in collectionView: NSCollectionView) -> Int {
return 1
}
// 2
public func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
// 3
public func collectionView(_ itemForRepresentedObjectAtcollectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let cell = resourceCollectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "SimpleCell"), for: indexPath) as! SimpleCell
return cell
}
public func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
print("selected item > ", indexPaths )
}
public func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
return NSSize(width: 10 , height:10)
}
}
Here is programmatically cell for NSCollectionView
class SimpleCell: NSCollectionViewItem {
private var containerView: NSView!
private var containBox: NSBox = {
var box = NSBox()
box.boxType = .custom
box.fillColor = NSColor.purple
box.translatesAutoresizingMaskIntoConstraints = false
return box
}()
override func loadView() {
containerView = NSView()
self.view = containerView
containerView.bounds = self.view.bounds
containerView.addSubview(containBox)
containBox.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
containBox.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
containBox.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
containBox.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true
}
}
resourceCollectionView not showing the cells and there is a strange behaviour such as when I add a line resourceCollectionView.reloadData() it shows up and then hides all cells automatically.
What is the missing points for the situation?
Thanks in advance
Edit:
I've solved my problem, when I show another NSViewController I used
self.view = anotherViewController.view
then no functions of NSCollectionView is triggered.
Then I changed it to
self.view.window?.contentViewController = anotherViewController
Then it worked.
I've solved my problem, when I show another NSViewController I used
self.view = anotherViewController.view
then no functions of NSCollectionView is triggered.
Then I changed it to
self.view.window?.contentViewController = anotherViewController
It solved my problem.