Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Views: complexity and reusability, experiments ...

Views: complexity and reusability, experiments comparing SwiftUI and UIKit

I presented this in Cocoaheads #51 in Oslo https://www.meetup.com/CocoaHeads-Oslo/events/268056965/

just sharing some insight i got while at work in FINN.no

The raw version of the slides can be found in https://github.com/fespinoza/Talks/blob/master/2020-02-13-view%20reusability/view-reusability.md

Felipe Espinoza

February 13, 2020
Tweet

More Decks by Felipe Espinoza

Other Decks in Technology

Transcript

  1. $ whomami • Felipe Espinoza • iOS Dev @ FINN.no

    • @fespinoza on github • @fespinozacast on twitter
  2. Creating views in UIKit • initialize • position / size1

    • styling • passing data • update to changes 1 size may depend on content
  3. class SampleViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor

    = .red let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "Hello" let button = UIButton() button.setTitle("Hello Who?", for: .normal) button.translatesAutoresizingMaskIntoConstraints = false button.addTarget(self, action: #selector(tap), for: .touchUpInside) self.view.addSubview(label) self.view.addSubview(button) NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 30), label.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 30), button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), ]) } }
  4. FinniversKit • FINN's UI component library • it's open source

    and it's awesome :D • FinniversKit on Github
  5. BetaFeatureView SettingDetailView BetaFeatureViewModel SettingDetailViewModel BetaFeatureViewDelegate SettingDetailViewDelegate • similar layout •

    they differ in data requirements and order of elements • delegates to bubble up touches on buttons • tons of similar non-shareable boilerplate
  6. Understanding UICollectionViewLayout To achieve the desired layout: • Do i

    use flow layout? • Do i subclass it? or the normal view layout? • Do i use the delegate methods? I need to make the cells's height to adjust to the content
  7. Understanding UICollectionViewLayout override func prepare() { super.prepare() itemAttributes = [UICollectionViewLayoutAttributes]()

    guard let collectionView = collectionView else { return } let columnsRange = 0 ..< configuration.numberOfColumns var columns = columnsRange.map { _ in 0 } var attributesCollection = [UICollectionViewLayoutAttributes]() var yOffset = configuration.topOffset if let height = delegate.adsGridViewLayout(self, heightForHeaderViewInCollectionView: collectionView) { let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: IndexPath(item: 0, section: 0)) attributes.frame = CGRect(x: 0, y: 0, width: collectionView.frame.size.width, height: height) attributesCollection.append(attributes) yOffset += height } for index in 0 ..< numberOfItems { let columnIndex = indexOfLowestValue(in: columns) let xOffset = xOffsetForItemInColumn(itemWidth: itemWidth, columnIndex: columnIndex) let topPadding = configuration.numberOfColumns > index ? yOffset : 0.0 let verticalOffset = CGFloat(columns[columnIndex]) + topPadding let indexPath = IndexPath(item: index, section: 0) let itemHeight = delegate.adsGridViewLayout(self, heightForItemWithWidth: itemWidth, at: indexPath) columns[columnIndex] = Int(verticalOffset + itemHeight + configuration.columnSpacing) let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) attributes.frame = CGRect(x: xOffset, y: verticalOffset, width: itemWidth, height: itemHeight) attributesCollection.append(attributes) } itemAttributes.append(contentsOf: attributesCollection) }
  8. But then... • The object page was a collection view

    • The cells need to calculate their height for the given width • My new component is supposed to be inside one of this cells • The height of the new table component was calculated asynchronously
  9. Second Attempt: UIStackView • The height of the component itself

    will be available synchronously when passing the data. • The integration to the object page doesn't require any hacks and it's easier.
  10. import SwiftUI struct AdView: View { let adViewModel: AdViewModel let

    imageDownloader: CollectionImageDownloader var body: some View { ScrollView { VStack(alignment: .leading, spacing: .spacingM) { imageGalleryView Group { titleView priceView sendMessageButton phoneNumber profileView addressView descriptionView conditionView }.padding(.horizontal) similarAdsView } } } }
  11. extension AdView { var titleView: some View { Text(adViewModel.title).title() }

    var priceView: some View { Text(adViewModel.price).titleStrong() } var sendMessageButton: some View { VStack(alignment: .center, spacing: .spacingS) { FINCTAButton { Text("Send melding") } Text(adViewModel.onwerAverageResponseTime).caption() } } var imageGalleryView: some View { GalleryView(imageDownloader: imageDownloader) } }
  12. Conditional Layout struct AdView: View { let adViewModel: AdViewModel var

    imageDownloader: CollectionImageDownloader { CollectionImageDownloader(adViewModel: adViewModel) } @Environment(\EnvironmentValues.horizontalSizeClass) var horizontalSizeClass var body: some View { ScrollView { if horizontalSizeClass == UserInterfaceSizeClass.compact { compactLayout } else { regularLayout } } } }
  13. Conditional Layout struct AdView: View { let adViewModel: AdViewModel var

    imageDownloader: CollectionImageDownloader { CollectionImageDownloader(adViewModel: adViewModel) } @Environment(\EnvironmentValues.horizontalSizeClass) var horizontalSizeClass var body: some View { ScrollView { if horizontalSizeClass == UserInterfaceSizeClass.compact { compactLayout } else { regularLayout } } } }
  14. Conditional Layout struct AdView: View { let adViewModel: AdViewModel var

    imageDownloader: CollectionImageDownloader { CollectionImageDownloader(adViewModel: adViewModel) } @Environment(\EnvironmentValues.horizontalSizeClass) var horizontalSizeClass var body: some View { ScrollView { if horizontalSizeClass == UserInterfaceSizeClass.compact { compactLayout } else { regularLayout } } } }
  15. Conditional Layout extension AdView { var compactLayout: some View {

    VStack(alignment: .leading, spacing: .spacingM) { imageGalleryView Group { titleView priceView sendMessageButton phoneNumber // same as before.. }.padding(.horizontal) similarAdsView } } }
  16. Conditional Layout extension AdView { var regularLayout: some View {

    VStack(alignment: .leading, spacing: .spacingM) { imageGalleryView HStack(alignment: .top, spacing: .spacingM) { VStack(alignment: .leading, spacing: .spacingM) { Group { titleView priceView addressView descriptionView // ... }.padding(.horizontal) } VStack(alignment: .leading, spacing: .spacingM) { profileView sendMessageButton phoneNumber } } similarAdsView } } }
  17. Recap (or just stating the obvious) • use the right

    tool for the job (I'm looking at you UICollectionView) • SwiftUI declarative syntax makes creating small/reusable components way better than UIKit • multiple simple components, over single "universal" component • ☝ together with top level decisions, make implementing different layouts/scenarios easy.
  18. Bonus • Attemp to use iOS 13 UICollectionView features while

    supporting iOS 11+: https://github.com/finn-no/ FinniversKit/pull/759