How to reload collectionview in UIViewRepresentable SwiftUI - ios

## How to reload collection view in UIViewRepresentable ##
Working with UIViewRepresentable and collection view, got stuck when its comes to reload() collection view after iterating, how to reload collection view in UIViewRepresentable when performing iterate through data? func updateUIView doesn't do the work.
struct VideoCollectionView: UIViewRepresentable {
var data: VideoViewModel
#Binding var search: String
var dataSearch: [VideoPostModel] {
if search.isEmpty {
return data.postsSearch
}else{
let d = data.postsSearch.filter {$0.artistname.localizedStandardContains(search)}
return d
}
}
var didSelectItem: ((_ indexPath: IndexPath)->()) = {_ in }
var didSelectObject: ((_ boject: VideoPostModel)->()) = {_ in }
func makeUIView(context: Context) -> UICollectionView {
let reuseId = "AlbumPrivateCell"
let collection :UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.sectionHeadersPinToVisibleBounds = true
let collectionV = UICollectionView(frame: .zero, collectionViewLayout: layout)
layout.scrollDirection = .vertical
collectionV.translatesAutoresizingMaskIntoConstraints = false
collectionV.backgroundColor = .clear
collectionV.dataSource = context.coordinator
collectionV.delegate = context.coordinator
collectionV.register(AlbumPrivateCell.self, forCellWithReuseIdentifier: reuseId)
return collectionV
}()
return collection
}
func updateUIView(_ collectionView: UICollectionView, context: UIViewRepresentableContext<VideoCollectionView>) {
print("updateUIView updateUIView")
print(search)
collectionView.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var parent: VideoCollectionView
init(_ albumGridView: VideoCollectionView) {
self.parent = albumGridView
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.parent.dataSearch.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AlbumPrivateCell", for: indexPath) as! AlbumPrivateCell
cell.data = self.parent.dataSearch[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let post = self.parent.dataSearch[indexPath.item]
parent.didSelectItem(indexPath)
parent.didSelectObject(post)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width
let height = collectionView.frame.height/2
return CGSize(width: width, height: height)
}
}
}

Dealing with this right now, only way i was able to make a reloadData, was creating a reference inside my Coordinator class and pass it on creation:
context.coordinator.collectionView = collectionView
From there you can make the call to reloadData(), pretty sure there are more elegant ways to work this though.
EDIT: Answering Request to expand on my approach.
Let's say you have a coordinator like this:
class CoordinatorItemPicturesView: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
// MARK: - Properties
var collectionView: UICollectionView!
var elements = [String]()
init(elements:[String]) {
self.elements = elements
}
// MARK: UICollectionViewDataSource
func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
return elements.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return UICollectionViewCell()
}
}
So when you create your UIViewRepresentable, pass the collectionView reference:
struct ItemPicturesView: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<ItemPicturesView>) -> UICollectionView {
/// Set Context, cells, etc
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.backgroundColor = .clear
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.isScrollEnabled = true
// --- Right here
context.coordinator.collectionView = collectionView
return collectionView
}
func updateUIView(_ uiView: UICollectionView, context _: Context) {
UIView.animate(withDuration: 1.0) {
uiView.frame = uiView.frame
uiView.reloadData()
}
}
func makeCoordinator() -> CoordinatorItemPicturesView {
CoordinatorItemPicturesView(elements:["Element One", "Element Two"])
}
}
I'm sorry, if the code is not 100% perfect, had to take it from a project with NDA and didn't test with the deleted properties.

UIViewRepresentable is just a wrapper.
you have to make ur model as Observable. then it will automatically update whenever any change in data.

All you need to do is, make your data property in VideoCollectionView as:
#Binding var data: VideoViewModel
Now once you update data, you update view will reload collectionView.
Explanation:
Your data property(value type) should be a Binding property as the Coordinator depends on that. When you init Coordinator with self, you are passing the old instance of data property. Changing it to binding allow it to be mutated.

Related

How to make UICollectionView dynamic height?

How to make UICollectionView dynamic height? The height of the UICollectionView should depend on the number of cells in it.
class ProduitViewController: UIViewController {
var productCollectionViewManager: ProductCollectionViewManager?
var sizeCollectionViewManager: SizeCollectionViewManager?
var product: ProductModel?
var selectedSize: String?
#IBOutlet weak var productCollectionView: UICollectionView!
#IBOutlet weak var sizeCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
}
private extension ProduitViewController {
func setup() {
guard let product = product else { return }
colorNameLabel.text = product.color[0].name
sizeCollectionViewManager = SizeCollectionViewManager.init()
sizeCollectionView.delegate = sizeCollectionViewManager
sizeCollectionView.dataSource = sizeCollectionViewManager
sizeCollectionViewManager?.set(product: product)
sizeCollectionViewManager?.didSelect = { selectedSize in
self.selectedSize = selectedSize
}
sizeCollectionView.reloadData()
}
}
Collection View Manager
import UIKit
final class SizeCollectionViewManager: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
var sizeProduct: [SizeModel] = []
var didSelect: ((String) -> Void)?
func set(product: ProductModel) {
sizeProduct = product.size
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return sizeProduct.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SizeCell", for: indexPath) as? SizeCollectionViewCell {
cell.configureCell(cellModel: sizeProduct[indexPath.row])
return cell
}
return UICollectionViewCell.init()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width / 3 + 20
let height: CGFloat = 35
return CGSize(width: width, height: height)
}
}
The height is now 35. If it is not set static, then the collection view will disappear altogether from the screen.
Screenshot Storyboard
You should set a height constraint on the UICollectionView reference. Once the constraint is set, you can calculate and set the constraint value based on number of objects, since you know how many rows it should display.

Start playling Lottie once collection view is changed

I have problem with lottie animations, I have some kind of onboarding on my app and what I would like to achive is to everytime view in collectionview is changed, to start my animation, I have 4 pages and 4 different lottie animations. Currently if I call animation.play() function, once app is started, all of my animations are played at the same time, so once I get to my last page, animation is over. And I want my lottie to be played only once, when view is shown.
This is my cell
class IntroductionCollectionViewCell: UICollectionViewCell {
#IBOutlet var title: UILabel!
#IBOutlet var subtitleDescription: UILabel!
#IBOutlet var animationView: AnimationView!
override func awakeFromNib() {
super.awakeFromNib()
}
public func configure(with data: IntroInformations) {
let animation = Animation.named(data.animationName)
title.text = data.title
subtitleDescription.text = data.description
animationView.animation = animation
}
static func nib() -> UINib {
return UINib(nibName: "IntroductionCollectionViewCell", bundle: nil)
}
}
This is how my collection view is set up
extension IntroductionViewController {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
pages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "IntroductionCollectionViewCell", for: indexPath) as! IntroductionCollectionViewCell
cell.configure(with: pages[indexPath.row])
cell.animationView.loopMode = .loop
cell.animationView.play()
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollPos = scrollView.contentOffset.x / view.frame.width
self.pageControl.currentPage = Int(floorf(Float(scrollPos)))
let n = pageControl.numberOfPages
if self.pageControl.currentPage == n - 1 {
continueButton.isHidden = false
} else {
continueButton.isHidden = true
}
}
}
Thanks in advance!!
You can use the collectionViewDelegate willDisplay and didEndDisplaying methods to start/stop the animations. And not when configuring the cell. - https://developer.apple.com/documentation/uikit/uicollectionviewdelegate
If you want the animation to run only once dont use the loop option.
if let cell = cell as? IntroductionCollectionViewCell {
cell.animationView.loopMode = .playOnce
cell.animationView.play()
}
this is the answer, I need to check is cell once is loaded my cell where lottie animation is

CollectionView : UIViewRepresentable + NavigationView

I use SwiftUI and this is UIViewRepresentable. I did CollectionView by this way. When I try to. add NavigationView in the controller, it works, but incorrect. When I scroll, the space between the collectionView and the navigationView is freed up.
#State var data:[Int] = [1,2,3,4,5,6,7,8,9,0,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]
var didSelectItem: ((_ indexPath: IndexPath)->()) = {_ in }
var didSelectObject: ((_ boject: Recipe)->()) = {_ in }
func makeUIView(context: Context) -> UICollectionView {
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "myCell")
collectionView.backgroundColor = .clear
collectionView.alwaysBounceVertical = true
return collectionView
}
func updateUIView(_ uiView: UICollectionView, context: Context) {
uiView.reloadData()
}
func makeCoordinator() -> Coordinator {
return Coordinator(data: data)
}
class Coordinator: NSObject, UICollectionViewDelegate, UICollectionViewDataSource {
var data: [Int]
init(data: [Int]) {
self.data = data
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath)
let textLable = UILabel()
textLable.text = String(data[indexPath.row])
cell.addSubview(textLable)
cell.backgroundColor = .red
return cell
}
//something more
private func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
print("User tapped on item \(indexPath.row)")
}
}
this is code represent collection view with NavigationView:
VStack {
NavigationView {
HStack {
MenuController()
}.navigationBarTitle("Menu")
}
}
Basically, it should work good, but what did I do wrong?
You have to call .edgesIgnoringSafeArea(.top) and it will do the trick by removing extra space from your collectionView.
Below is your code looks like after the change.
struct CollectionContainer: View {
var body: some View {
VStack {
NavigationView {
HStack {
MenuController()
}.navigationBarTitle("Menu")
.edgesIgnoringSafeArea(.top) // Trick
}
}
}
}
Hope it will help you.

Scrolling issue with UITableViewController hosted in UICollectionViewCell

I have the following setup in my app:
UITabBarController
UINavigationController
UIViewController
The UIViewController has a UICollectionView with horizontal scrolling.
In the cells, I want to "host" a view from another ViewController. This works pretty well, but I have scrolling issues. The first UICollectionViewCell hosts a view that comes from a UITableViewController. I can scroll the UITableViewController but it does not really scroll to the end - it seems like the UITableViewController starts to bounce way too early.
When I used the UITableViewController as the Root View Controller, everything worked fine, so I don't think there is something wrong with this ViewController.
The height of the CollectionView is pretty small, I just wanted to show the "bouncing" behaviour.
Here is the code for the collectionView:
import Foundation
import UIKit
class FeedSplitViewController : UIViewController, Controllable
{
#IBOutlet weak var menuBar: MenuBar!
#IBOutlet weak var collectionView: UICollectionView!
private var currentIndex = 0
private var dragStart: CGFloat = 0.0
private var feedActivities: FeedViewController!
var controller: Controller!
override func viewDidLoad()
{
super.viewDidLoad()
self.initControls()
self.initMenuBar()
self.initCollectionView()
self.initActivitiesViewController()
}
fileprivate func initActivitiesViewController()
{
self.feedActivities = UIStoryboard.instantiate("Main", "feedActivities")
self.feedActivities.controller = self.controller
}
fileprivate func initControls()
{
self.navigationController?.navigationBar.setValue(false, forKey: "hidesShadow")
}
fileprivate func initMenuBar()
{
self.menuBar.showLine = true
self.menuBar.enlargeIndicator = true
self.menuBar.texts = [Resources.get("FEED_ACTIVITIES"), Resources.get("DASHBOARD")]
self.menuBar.selectionChanged =
{
index in
self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: UICollectionView.ScrollPosition.right, animated: true)
}
}
fileprivate func initCollectionView()
{
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
let menuBarFrame = self.menuBar.frame.origin
let collectionView = self.collectionView.frame.origin
Swift.print(menuBarFrame)
Swift.print(collectionView)
}
}
extension FeedSplitViewController : UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
if indexPath.item == 0, let feedActivities = self.feedActivities
{
cell.contentView.addSubview(feedActivities.view)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat
{
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: self.view.bounds.width, height: self.view.bounds.height)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView)
{
self.dragStart = scrollView.contentOffset.x
}
func scrollViewDidScroll(_ scrollView: UIScrollView)
{
let oldIndex = self.currentIndex
let page = scrollView.contentOffset.x / scrollView.frame.size.width
let currentPage = Int(round(page))
if oldIndex != currentPage
{
if Settings.useHapticFeedback
{
Utilities.haptic(.medium)
}
self.menuBar.selectedIndex = currentPage
}
self.currentIndex = currentPage
}
}
I have attached a small video: https://imgur.com/a/pj7l3Hd
I solved it by doing the following:
I no longer host the view of an ViewController directly in the
Every UICollectionView cell hosts an UITableView.
The UITableViewCell contains the data model that was previously implemented in the ViewController. The logic is still outside of the UITableViewCell.

UICollectionView duplicates cells when returning from another view controller

I have been working on a weather app and been using UICollectionView to display weather data.
Whenever I open another view controller and return back to the UICollectionView's View controller, I get duplicate cells.
Here is the code.
I use Alamofire to make api requests, append the json result to a local string and then assign it to the cell's text labels.
class VaanizhaiViewController: UICollectionViewController {
// MARK: - flow layout
let columns : CGFloat = 2.0
let inset : CGFloat = 3.0
let spacing : CGFloat = 3.0
let lineSpacing : CGFloat = 3.0
var isRandom : Bool = false
// MARK: - Variables
var apiKey: String = "55742b737e883a939913f2c90ee11ec0"
var country : String = ""
var zipCode : String = ""
var json : JSON = JSON.null
var cityNames: [String] = []
var temperature: [Int] = []
var weather: [String] = []
// MARK: - Actions
// MARK: - view did load
override func viewDidLoad() {
super.viewDidLoad()
let layout = BouncyLayout()
self.collectionView?.setCollectionViewLayout(layout, animated: true)
self.collectionView?.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
for i in 0...zip.count-1{
parseURL(zipCode: "\(zip[i])", country: "us")
}
}
// MARK: - Functions
func parseURL(zipCode: String, country: String){
let url = "http://api.openweathermap.org/data/2.5/weather?zip=\(zipCode),\(country)&APPID=\(apiKey)&units=metric"
requestWeatherData(link: url)
}
func requestWeatherData(link: String){
Alamofire.request(link).responseJSON{ response in
if let value = response.result.value{
self.json = JSON(value)
self.cityNames.append(self.json["name"].string!)
let cTemp = ((self.json["main"]["temp"].double)!)
self.temperature.append(Int(cTemp))
let cWeather = self.json["weather"][0]["main"].string!
self.weather.append(cWeather)
self.collectionView?.reloadData()
}
}
}
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.cityNames.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "weatherCell", for: indexPath) as! WeatherViewCell
if !self.cityNames.isEmpty {
cell.cityLabel.text = self.cityNames[indexPath.row]
cell.tempLabel.text = String (self.temperature[indexPath.row])
cell.weatherLabel.text = self.weather[indexPath.row]
cell.backgroundColor = UIColor.brown
}
return cell
}
// MARK: - UICollectionViewDelegateFlowLayout
extension VaanizhaiViewController: UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = Int ((CGFloat(collectionView.frame.width) / columns) - (inset + spacing))
return CGSize(width: width, height: width)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: CGFloat(inset), left: CGFloat(inset), bottom: CGFloat(inset), right: CGFloat(inset))
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return spacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return lineSpacing
}
}
Your viewDidAppear will be called every time you visit the view controller, and your viewDidLoad will only get called once, when it's created.
Therefore, you should probably switch your call to self.collectionView?.reloadData() to the viewDidAppear and your call to parseURL to the viewDidLoad
if you need to update your weather data (on a refresh, for example), then you need to restructure your requestWeatherData to stop appending to your arrays, and replace instead.

Resources