iOS UIKit: Compositional Layout Kullanımı

iOS UIKit ile Compositional Layout‘un nasıl kullanıldığını bu içerikte detaylı bir şekilde anlatmaya çalışacağım. Compositional Layout, iOS 13 ile birlikte gelen CollectionView’ın daha esnek ve daha modüler bir yapıda olmasına olanak sağlayan bir yapıdır.

Bu yapı ile birlikte oldukça karmaşık bir sayfayı bile hücrelere bölerek tasarlayabilir, dinamik uzunlukta listeler elde edebiliriz. Benzer sonuçları ScrollView kullanarak da alabiliriz ancak bir takım sabit uzunluklar vermek ve bazı tasarımların farklılığından dolayı ScrollView ile istediğimiz sonuçları almamız pek mümkün olmuyor.

İşte bu noktada sayfamızı tasarlarken Compositional Layout’dan faydalanıp tek bir CollectionView kullanarak farklı tasarımları entegre edebiliriz. Peki bu yapının bizim açımızdan avantaj ve dezavantajları nedir bir bakalım.

Avantajları

  • Esneklik: Compositional Layout’un en önemli faydalarından birisi kesinlikle esnekliktir. En karmaşık ekranları bile tasarlarken büyük bir kolaylık sağlar.
  • Detaylandırılabilir: Her hücre, grup ve bölüm birbirinden farklı şekilde dizayn edilebilmektedir.
  • Performans: Bu konuda da Compositional Layout bize büyük bir fayda sağlıyor çünkü tek bir CollectionView nesnesi üzerinden sayfada yer alacak olan tüm kaydırılabilir nesneleri hareket ettirebiliyoruz. Unutmayın bir sayfa içerisinde ne kadar az kaydırılabilir eleman varsa o kadar yüksek performans alırız. Bir ScrollView içerisinde CollectionView koymanın performans açısından bir çok dezavantajı vardır.

Dezavantajları

  • Zorluk Seviyesi: İlk etapta öğrenmesi ve özümsemesi zaman alan bir yapı ancak bir kaç basit örnek ve birazcık çaba ile hiçte zor olmadığını sizde birazdan göreceksiniz.
  • Zaman: Basit, bir kaç ekranlı uygulamalar için karmaşıklık düzeyi biraz yüksek olduğundan gereksiz vakit uzatabilir.

Avantaj ve dezavantajlarına değindikten sonra kendimce ve uzun bir süre yaptığım araştırmalar sonucunda şunu net bir şekilde söyleyebilirim ki: Tek bir ScrollView içerisine elemanları dolduru, sayfayı kaydırmak yerine tek bir CollectionView ile tüm sayfayı kontrol etmek çok daha iyi bir seçim.

Şimdi Compositional Layout ile neler yapabileceklerimize bir bakalım.

Trendyol, Hepsiburada, Amazon, YouTube gibi birden fazla component içeren karmaşık ekranları oluşturabiliriz.

Normalde bu karmaşıklıkta bir ekranı ScrollView kullanarak yapmak oldukça zor. Bir de ScrollView içerisinde dinamik uzunlukta dikey listelenen bir CollectionView kullanmak istediğinizde durum iyice çığırından çıkar ve saç baş yolmaya başlarsınız. Daha fazla uzatmadan bir örnek yapalım. Bu örneğimizde en üstte bir header, altında soldan sağa kayan bir kategori listesi, onun da altında ürünler listesi yacağız.

  • Bir tane UICollectionView oluşturalım
    let myCollectionView : UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.register(UINib(nibName: "TopMenuCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "TopMenuCollectionViewCell")
        collectionView.register(UINib(nibName: "CategoryCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "CategoryCollectionViewCell")
        return collectionView
    }()

CollectionView’ımızı oluşturup, içerisine iki tane cell ekledik. Bu cell’leri XIB ile oluşturduk. Bir sonraki örnekte cell’leri programatik olarak nasıl yapacağımızı da inceleyeceğiz.

  • ViewDidLoad’a bir kaç ekleme yapalım
  override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(myCollectionView)
        myCollectionView.delegate = self
        myCollectionView.dataSource = self
        setupConstraint()
        setupCompositionalLayout()
    }

Burada oluşturduğumuz CollectionView’ı ana view’ımıza ekledik ve gerekli ayarlamalarını yaptık. Bu kısımları hızlı geçiyorum çünkü önceki makalelerde CollectionView Kullanımı hakkında detaylı bir paylaşım yapmıştım.

  • setupConstraint fonksiyonumuz;
    func setupConstraint(){
        NSLayoutConstraint.activate([
            myCollectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            myCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            myCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            myCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }

CollectionView’ın ekrana tam bir şekilde oturmasını sağlayarak geniş bir görünüm elde ettik.

Şimdi bundan sonraki kısımda asıl büyük işlemin setupCompositionalLayout() fonksiyonu ile yapıldığını göreceğiz. Bir çok detay burada yer alıyor bu noktaya kadar geldiyseniz şimdi çok daha dikkatli bir şekilde takip etmenizde fayda var.

  • setupCompositionalLayout fonksiyonu
 func setupCompositionalLayout(){
        let layout = UICollectionViewCompositionalLayout{ sectionIndex, environmentData in
            if(sectionIndex == 0){
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                
                let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
                
                let section = NSCollectionLayoutSection(group: group)
                return section
                
            }else if sectionIndex == 1 {
                let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(150), heightDimension: .estimated(40))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                
                let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(150), heightDimension: .estimated(40))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
                
                let section = NSCollectionLayoutSection(group: group)
                section.orthogonalScrollingBehavior = .continuous
               section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
                return section
            }
            
            else{
                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(200))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                
                let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(200))
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
                
                let section = NSCollectionLayoutSection(group: group)
    
                return section
            }
            
        }
        myCollectionView.setCollectionViewLayout(layout, animated: true)
    }

Bu kadar kodu birden görünce gözünüz korkmasın 😀 Şimdi tek tek hangi satır ne işe yarıyor hepsini anlatacağım.

Öncelikle Compositional Layout’un nasıl çalıştığından kısaca bahsedeyim ki kodları anlamamız bir o kadar kolay olsun.

Compositional Layout’da temel olarak dikkat etmemiz gereken 3 yapı var. Item, Group ve Section. Şimdi aşağıdaki görsel ile bu üç yapının ne olduğunu net bir şekilde anlayalım.

En dıştaki kapsayıcı yapımız Section, onun içinde yer alan ve tüm item’ları tutan yapımız Group ve her bir kırmızı yapı da Item‘larımızı temsil ediyor.

Satır Satır Gidelim

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
return section

NSCollectionLayoutSize: Her bir kırmızı öğenin boyutunu veriyoruz. Ekranda yer alacak olan her item’ın boyutunu NSCollectionLayoutSize kullanarak belirtiyoruz. Peki bu içlerinde kullandığımız fractionalWidth ve absolute gibi değerler nedir? Hepsini açıklayayım.

Compositional Layout’da bir boyut verirken üç farklı şekilde verebiliyoruz. Bunlar;

absolute: Öğenin gerçek net değerini biliyorsak kullanırız. Mesela bir ikon ekledim ve boyutu kesinlikle 50×50 olacağını biliyorum. O zaman widthDimension ve heightDimension parametrelerine .absolute(50) değeri veriyoruz.

estimated: Bir öğenin boyutunu tam olarak bilmiyorsak yaklaşık bir değer verip, layout oluşturulduğunda otomatik olarak boyutlandırılmasını sağlıyor.

fractionalWidth / fractionalHeight: Yüzdelik olarak boyut vermemizi sağlıyor. Width kısmında 0.4 verirsek ekran genişliğin yüzde 40’ını, yükseklik kısmında 0.6 yazarsak ekran yüksekliğinin yüzde 60’ını boyut olarak alır.

NSCollectionLayoutItem: Her bir item için bir layout oluşturup, o layout’un size’ını az önce oluşturduğumuz parametre ile veriyoruz. Yukarıdaki kod satırında şunu demiş olduk: genişliğimi ekran genişliğinin yüzde yüzü olarak al, yüksekliği de net olarak 50 olarak ver.

NSCollectionLayoutGroup.horizontal: Burada ise item’leri tutacak olan grubumuzu tanımlıyoruz. Bir üst satırda genişlik ve yükseklik satırını verdikten sonra elemanların bir grup içerisinde kaç eleman olacağını da belirttik. subItems parametresiyle bir grup içinde kaç eleman olacağını belirtebiliyoruz. Eğer subItems[item,item] yaparsak bir satırda iki eleman görünecektir.

Eğer dikey bir listeleme yapmak istiyorsak NSCollectionLayoutGroup.vertical kullanabiliriz.

NSCollectionLayoutSection: Section ise bizim her bir bölümümüzü ifade ediyor. CollectionView ile daha önce bir kaç örnek yaptıysanız daha önce görmüşsünüzdür. Varsayılan olarak section sayısı 1 gelir ve tüm liste aynı dizayn elemanları gösterir. Ancak section sayısını arttırdığımızda listeye bölümler eklemiş oluruz. Bu eklediğimiz bölümleri de kod içerisinden yakalayarak hangi tasarımda cell göstereceğimize karar veriririz.

Kodun genel bir açıklamasını yaparksak;

UICollectionViewCompositionalLayout tipinde bir layout değişkeni tanımladım ve closure olarak başlatıp bana gelecek olan iki tane parametreye isim verdim. sectionIndex ve environmentData ile UICollectionView tarafından gönderilecek olan parametreleri aldım.

SectionIndex ile hangi bölümde olduğumuz tespit edip, o bölümlere uygun boyut ayarlamalarını gerçekleştirip, en son setCollectionViewLayout fonksiyonunu kullanıp, CollectionView’ıma Compositional Layout’u aktarmış oldum.

CollectionView için yazmamamız gereken zorunlu fonksiyonlar

extension ViewController : UICollectionViewDataSource, UICollectionViewDelegate {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        if(section == 0){
            return 1
        }else if section == 1{
            return categoryList.count
        }
        else{
            return 5
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        if(indexPath.section == 0 ){
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TopMenuCollectionViewCell", for: indexPath) as! TopMenuCollectionViewCell
            return cell
        }else if indexPath.section == 1 {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CategoryCollectionViewCell", for: indexPath) as! CategoryCollectionViewCell
            cell.categoryLabel.text = categoryList[indexPath.row].categoryName
            cell.categoryImage.image = UIImage(named: categoryList[indexPath.row].categoryImage ?? "")
            return cell
        }
        else{
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TopMenuCollectionViewCell", for: indexPath) as! TopMenuCollectionViewCell
            return cell
        }
    }
}

Burada dikkat ettiyseniz section sayısını 2 verdim çünkü 2 tane bölüm olsun istedim.

cellForItemAt fonksiyonunda ise indexPath.section üzerinden hangi section’da olduğumu anlayıp, o sectionlara özel cell tasarımlarımı göstermiş oldum.

Cell tasarımlarına çok fazla değinmedim ancak hepsini XIB ile yapıp, CollectionView’a ekledim. Uygulamanın son çıktısı görseldeki gibi oldu.

Çok daha detaylı ve karmaşık ekran tasarımlarının nasıl yapılacağınıda paylaşacağım. Şimdilik yazıyı burada sonlandırıyorum. Kafanızı karıştıran, anlamadığınız noktalar olursa lütfen yorumlarda belirtin.