diff --git a/README.md b/README.md index 334ab6d..1fe3a73 100644 --- a/README.md +++ b/README.md @@ -104,28 +104,34 @@ class ViewController: UIViewController { **** -# oneleif Project +## oneleif project +This means that the project is sponsored by the oneleif community, and the collaborators are team members from oneleif. -![](https://github.com/oneleif/olDocs/blob/master/assets/images/oneleif_logos/full_logo/oneleif_whiteback.png) +![](https://github.com/oneleif/olDocs/blob/master/assets/images/oneleif_logos/full_logo/oneleif_whiteback.png) -### Project Info -This project is a oneleif active project. [![](https://img.shields.io/badge/oneleif-Twitter-blue.svg)](https://twitter.com/oneleifdev) [![](https://img.shields.io/badge/oneleif-YouTube-red.svg)](https://www.youtube.com/channel/UC3HN0jID38K0Vb_WChvgQmA) +## What is oneleif? +oneleif is a nonprofit community that supports tech minded individuals. We do this by offering a fun loving community that works on Open Sourced projects together. +We love to give back through free resources and guidance. + ## How to join oneleif Click on the link below to join the Discord server. -You will start with limited permissions, in a text channel that only moderators will see. +[![](https://img.shields.io/badge/oneleif-Discord-7284be.svg)](https://discord.gg/tv9UdJK) -To get full access: read the rules, make an introduction in #introductions, and add an appropriate username. +-OR- -When you're done with the above, shoot a message to the #start channel to let us know, and we will give you full access. +[Check out our website](http://oneleif.com) -[![](https://img.shields.io/badge/oneleif-Discord-7284be.svg)](https://discord.gg/tv9UdJK) ### Questions? Feel free to email us at: oneleifdev@gmail.com + +-OR- + +Ask questions in the discord diff --git a/Sources/SwiftUIKit/Extensions/NSLayoutConstraint+SwiftUIKit.swift b/Sources/SwiftUIKit/Extensions/NSLayoutConstraint+SwiftUIKit.swift new file mode 100644 index 0000000..d1934b1 --- /dev/null +++ b/Sources/SwiftUIKit/Extensions/NSLayoutConstraint+SwiftUIKit.swift @@ -0,0 +1,49 @@ +// +// NSLayoutConstraint+SwiftUIKit.swift +// SwiftUIKit +// +// Created by Zach Eriksen on 5/17/20. +// + +import UIKit + +@available(iOS 10.0, *) +public extension NSLayoutConstraint { + + /// Check if the `constraint` is connected to the `anchor` + func isConnected(toAnchor anchor: NSLayoutAnchor) -> Bool { + firstAnchor == anchor || secondAnchor == anchor + } +} + +@available(iOS 10.0, *) +public extension UIView { + + /// The leading constraints held by the view + var leadingConstraints: [NSLayoutConstraint] { + constraints.filter { (constraint) -> Bool in + constraint.isConnected(toAnchor: leadingAnchor) + } + } + + /// The trailing constraints held by the view + var trailingConstraints: [NSLayoutConstraint] { + constraints.filter { (constraint) -> Bool in + constraint.isConnected(toAnchor: trailingAnchor) + } + } + + /// The top constraints held by the view + var topConstraints: [NSLayoutConstraint] { + constraints.filter { (constraint) -> Bool in + constraint.isConnected(toAnchor: topAnchor) + } + } + + /// The bottom constraints held by the view + var bottomConstraints: [NSLayoutConstraint] { + constraints.filter { (constraint) -> Bool in + constraint.isConnected(toAnchor: bottomAnchor) + } + } +} diff --git a/Sources/SwiftUIKit/Extensions/UIView+SwiftUIKit.swift b/Sources/SwiftUIKit/Extensions/UIView+SwiftUIKit.swift index 72fa5c4..3289540 100644 --- a/Sources/SwiftUIKit/Extensions/UIView+SwiftUIKit.swift +++ b/Sources/SwiftUIKit/Extensions/UIView+SwiftUIKit.swift @@ -292,6 +292,48 @@ public extension UIView { return self } + /// Update a padding anchor's constant value + @available(iOS 10.0, *) + @discardableResult + func update(padding: Padding) -> Self { + switch padding { + case .top(let value): + topConstraints.first?.constant = CGFloat(value) + case .bottom(let value): + bottomConstraints.first?.constant = CGFloat(-value) + case .leading(let value): + leadingConstraints.first?.constant = CGFloat(value) + case .trailing(let value): + trailingConstraints.first?.constant = CGFloat(-value) + } + + return self + } + + /// Update an array of padding anchors' constant values + @available(iOS 10.0, *) + @discardableResult + func update(padding: [Padding]) -> Self { + padding.forEach { update(padding: $0) } + + return self + } + + /// Update all padding anchors' constant value + @available(iOS 10.0, *) + @discardableResult + func update(padding: Float) -> Self { + update(padding: [ + .top(padding), + .bottom(padding), + .leading(padding), + .trailing(padding) + ]) + + return self + } + + /// Remove the height anchor constraint @available(iOS 10.0, *) @discardableResult @@ -432,7 +474,7 @@ public extension UIView { /// - animated: Should animate setting the left UIBarButtonItem @discardableResult func navigateSetLeft(barButton: UIBarButtonItem?, animated: Bool = true) -> Self { - Navigate.shared.setLeft(barButton: barButton) + Navigate.shared.setLeft(barButton: barButton, animated: animated) return self } @@ -443,7 +485,7 @@ public extension UIView { /// - animated: Should animate setting the right UIBarButtonItem @discardableResult func navigateSetRight(barButton: UIBarButtonItem?, animated: Bool = true) -> Self { - Navigate.shared.setRight(barButton: barButton) + Navigate.shared.setRight(barButton: barButton, animated: animated) return self } @@ -454,7 +496,7 @@ public extension UIView { /// - animated: Should animate setting the left [UIBarButtonItem] @discardableResult func navigateSetLeft(barButtons: [UIBarButtonItem]?, animated: Bool = true) -> Self { - Navigate.shared.setLeft(barButtons: barButtons) + Navigate.shared.setLeft(barButtons: barButtons, animated: animated) return self } @@ -465,7 +507,7 @@ public extension UIView { /// - animated: Should animate setting the right [UIBarButtonItem] @discardableResult func navigateSetRight(barButtons: [UIBarButtonItem]?, animated: Bool = true) -> Self { - Navigate.shared.setRight(barButtons: barButtons) + Navigate.shared.setRight(barButtons: barButtons, animated: animated) return self } diff --git a/Sources/SwiftUIKit/Views/CollectionView.swift b/Sources/SwiftUIKit/Views/CollectionView.swift new file mode 100644 index 0000000..2f5014f --- /dev/null +++ b/Sources/SwiftUIKit/Views/CollectionView.swift @@ -0,0 +1,660 @@ +// +// Collection.swift +// SwiftUIKit +// +// Created by Oskar on 16/05/2020. +// + +import UIKit + +@available(iOS 11.0, *) +public typealias CollectionViewCell = DataIdentifiable & CellConfigurable & CellUpdatable & UICollectionViewCell + +@available(iOS 11, *) +public class CollectionView: UICollectionView { + public var data: [[CellDisplayable]] + + fileprivate var titles: [String]? = nil + fileprivate var sectionsInsets: [UIEdgeInsets]? = nil + fileprivate var footerSizeForSections: [CGSize]? = nil + fileprivate var headerSizesForSections: [CGSize]? = nil + fileprivate var minimumLineSpacingForSections: [CGFloat]? = nil + fileprivate var minimumInteritemSpacingForSections: [CGFloat]? = nil + + fileprivate var shouldSelectItemAtHandler: ((IndexPath) -> Bool)? = nil + fileprivate var didSelectItemAtHandler: ((IndexPath) -> ())? = nil + fileprivate var shouldDeselectItemAtHandler: ((IndexPath) -> Bool)? = nil + fileprivate var didDeselectItemAtHandler: ((IndexPath) -> ())? = nil + fileprivate var shouldHighlightItemAtHandler: ((IndexPath) -> Bool)? = nil + fileprivate var didHighlightItemAtHandler: ((IndexPath) -> ())? = nil + fileprivate var didUnhighlightItemAtHandler: ((IndexPath) -> ())? = nil + fileprivate var canFocusItemAtHandler: ((IndexPath) -> Bool)? = nil + + fileprivate var willDisplayCellHandler: ((CollectionViewCell, IndexPath) -> ())? = nil + fileprivate var didEndDisplayingCell: ((CollectionViewCell, IndexPath) -> ())? = nil + + fileprivate var numberOfItemsInSectionHandler: ((UICollectionView, Int) -> Int)? = nil + fileprivate var cellForItemAtHandler: ((UICollectionView, IndexPath) -> (UICollectionViewCell))? = nil + fileprivate var targetIndexPathForMoveHandler: ((_ from: IndexPath, _ to: IndexPath) -> IndexPath)? = nil + fileprivate var targetContentOffsetForHandler: ((_ proposed: CGPoint) -> CGPoint)? = nil + fileprivate var layoutSizeForItemHandler: ((UICollectionViewLayout, IndexPath) -> CGSize)? = nil + fileprivate var transitionLayoutForHandler: ((_ old: UICollectionViewLayout, _ new: UICollectionViewLayout) -> UICollectionViewTransitionLayout)? = nil + + fileprivate var didBeginMultipleSelectionInteractionAtHandler: ((IndexPath) -> ())? = nil + fileprivate var didEndMultipleSelectionInteractionHandler: ((UICollectionView) -> ())? = nil + fileprivate var shouldBeginMultipleSelectionInteractionAtHandler: ((IndexPath) -> Bool)? = nil + + public init(initialData: [[CellDisplayable]] = [[CellDisplayable]]()) { + data = initialData + + let layout = UICollectionViewFlowLayout() + layout.estimatedItemSize = .zero + + super.init(frame: .zero, collectionViewLayout: layout) + + delegate = self + dataSource = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Auxiliary modifiers +@available(iOS 11, *) +public extension CollectionView { + /// Set a new delegate for CollectionView. + /// All modifiers depending on delegate will do nothing after calling that. + @discardableResult + func set(delegateTo delegate: UICollectionViewDelegate) -> Self { + self.delegate = delegate + + return self + } + + /// Set a new delegate for CollectionView. + /// All modifiers depending on data source will do nothing after calling that. + @discardableResult + func set(dataSourceTo dataSource: UICollectionViewDataSource) -> Self { + self.dataSource = dataSource + + return self + } +} + +// MARK: - Update data and layout +@available(iOS 11.0, *) +public extension CollectionView { + /// Set a new layout for collection view. + @discardableResult + func set(layout: UICollectionViewLayout) -> Self { + setCollectionViewLayout(layout, animated: false) + + return self + } + + @discardableResult + func update(shouldReloadData: Bool = false, + _ closure: ([[CellDisplayable]]) -> [[CellDisplayable]]) -> Self { + data = closure(data) + + if shouldReloadData { + reloadData() + } + + return self + } + + @discardableResult + func append(shouldReloadData: Bool = false, + _ closure: () -> [[CellDisplayable]]) -> Self { + data += closure() + + if shouldReloadData { + reloadData() + } + + return self + } +} + +// MARK: - Prefetching +@available(iOS 11, *) +public extension CollectionView { + @discardableResult + func prefetchingEnabled(_ bool: Bool) -> Self { + isPrefetchingEnabled = bool + + return self + } + + @discardableResult + func prefetchDataSource(_ prefetchDataSource: () -> (UICollectionViewDataSourcePrefetching)) -> Self { + self.prefetchDataSource = prefetchDataSource() + + return self + } +} + +// MARK: - Creating Collection Cells +@available(iOS 11, *) +public extension CollectionView { + @discardableResult + func register(cells: [CollectionViewCell.Type]) -> Self { + cells.forEach { + super.register($0, forCellWithReuseIdentifier: $0.ID) + } + + return self + } +} + +// MARK: - Items management +@available(iOS 11, *) +public extension CollectionView { + @discardableResult + func insertItems(at indexPaths: [IndexPath]) -> Self { + super.insertItems(at: indexPaths) + + return self + } + + @discardableResult + func insertItem(at indexPath: IndexPath) -> Self { + super.insertItems(at: [indexPath]) + + return self + } + + @discardableResult + func moveItem(at sourceIndexPath: IndexPath, to targetIndexPath: IndexPath) -> Self { + super.moveItem(at: sourceIndexPath, to: targetIndexPath) + + return self + } + + @discardableResult + func moveItems(at sourceIndexPaths: [IndexPath], to targetIndexPaths: [IndexPath]) -> Self { + for (source, target) in zip(sourceIndexPaths, targetIndexPaths) { + super.moveItem(at: source, to: target) + } + + return self + } + + @discardableResult + func deleteItem(at indexPath: IndexPath) -> Self { + super.deleteItems(at: [indexPath]) + + return self + } + + @discardableResult + func deleteItems(at indexPaths: [IndexPath]) -> Self { + super.deleteItems(at: indexPaths) + + return self + } + + @discardableResult + func scrollToItem(at indexPath: IndexPath, at position: UICollectionView.ScrollPosition, animated: Bool = true) -> Self { + super.scrollToItem(at: indexPath, at: position, animated: animated) + + return self + } + + @discardableResult + func allowSelection(_ bool: Bool, multiple: Bool = false) -> Self { + allowsSelection = bool + allowsMultipleSelection = multiple + + return self + } + + @discardableResult + func selectItem(at indexPath: IndexPath, animated: Bool, scrollPosition: UICollectionView.ScrollPosition) -> Self { + super.selectItem(at: indexPath, animated: animated, scrollPosition: scrollPosition) + + return self + } + + @discardableResult + func deselectItem(at indexPath: IndexPath, animated: Bool) -> Self { + super.deselectItem(at: indexPath, animated: animated) + + return self + } +} + +// MARK: - Section Management +@available(iOS 11, *) +public extension CollectionView { + @discardableResult + func insertSections(at indexSet: IndexSet) -> Self { + super.insertSections(indexSet) + + return self + } + + @discardableResult + func moveSection(from section: Int, to newSection: Int) -> Self { + super.moveSection(section, toSection: newSection) + + return self + } + + @discardableResult + func deleteSections(at sections: IndexSet) -> Self { + super.deleteSections(sections) + + return self + } +} + + +// MARK: - Appearance customization +@available(iOS 11, *) +public extension CollectionView { + @discardableResult + func set(backgroundView + : UIView) -> Self { + self.backgroundView = backgroundView + + return self + } +} + +// MARK: - Data Source modifiers +@available(iOS 11, *) +public extension CollectionView { + /// - Parameter handler: Contains actually declared collection view and given section, expects to return number of items for given section. + @discardableResult + func setNumberOfItemsInSection(handler: @escaping ((UICollectionView, Int) -> Int)) -> Self { + numberOfItemsInSectionHandler = handler + + return self + } + + @discardableResult + func configureCell(handler: @escaping ((UICollectionView, IndexPath) -> UICollectionViewCell)) -> Self { + cellForItemAtHandler = handler + + return self + } + + /// Set index titles for CollectionView + /// - Parameter array: Used to provide titles for Collection View, order used is the same as the order of parameter. + @discardableResult + func setSectionTitles(shouldReloadData: Bool = false, to array: [String]) -> Self { + titles = array + + if shouldReloadData { + reloadData() + } + + return self + } +} + +// MARK: - Auxiliary Data Source +@available(iOS 11, *) +extension CollectionView: UICollectionViewDataSource { + public func numberOfSections(in collectionView: UICollectionView) -> Int { + return data.count + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return data[section].count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cellData = data[indexPath.section][indexPath.row] + + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellData.cellID, for: indexPath) + + if let configure = cell as? CellUpdatable { + configure.update(forData: cellData) + } + + guard cell.contentView.allSubviews.count == 0 else { + return cell + } + + if let configure = cell as? CellConfigurable { + configure.configure(forData: cellData) + } + + return cell + } + + public func indexTitles(for collectionView: UICollectionView) -> [String]? { + return titles + } + + public func collectionView(_ collectionView: UICollectionView, indexPathForIndexTitle title: String, at index: Int) -> IndexPath { + IndexPath(index: index) + } +} + +// MARK: - Delegation modifiers +@available(iOS 11, *) +public extension CollectionView { + @discardableResult + func shouldSelectItem(_ handler: @escaping ((IndexPath) -> Bool)) -> Self { + shouldSelectItemAtHandler = handler + + return self + } + + @discardableResult + func didSelectItemAt(_ handler: @escaping ((IndexPath) -> ())) -> Self { + didSelectItemAtHandler = handler + + return self + } + + @discardableResult + func shouldDeselectItemAt(_ handler: @escaping ((IndexPath) -> Bool)) -> Self { + shouldDeselectItemAtHandler = handler + + return self + } + + @discardableResult + func didDeselectItemAt(_ handler: @escaping ((IndexPath) -> ())) -> Self { + didDeselectItemAtHandler = handler + + return self + } + + @discardableResult + func shouldBeginMultipleSelectionInteractionAt(_ handler: @escaping ((IndexPath) -> Bool)) -> Self { + shouldBeginMultipleSelectionInteractionAtHandler = handler + + return self + } + + @discardableResult + func didBeginMultipleSelectionInteractionAt(_ handler: @escaping ((IndexPath) -> ())) -> Self { + didBeginMultipleSelectionInteractionAtHandler = handler + + return self + } + + @discardableResult + func didEndMultipleSelectionSelectionInteraction(_ handler: @escaping ((UICollectionView) -> ())) -> Self { + didEndMultipleSelectionInteractionHandler = handler + + return self + } + + @discardableResult + func shouldHighlightItemAt(_ handler: @escaping ((IndexPath) -> Bool)) -> Self { + shouldHighlightItemAtHandler = handler + + return self + } + + @discardableResult + func didHighlightItemAt(_ handler: @escaping ((IndexPath) -> ())) -> Self { + didHighlightItemAtHandler = handler + + return self + } + + @discardableResult + func didUnhighlightItemAt(_ handler: @escaping ((IndexPath) -> ())) -> Self { + didUnhighlightItemAtHandler = handler + + return self + } + + @discardableResult + func willDisplayCellHandlerFor(_ handler: @escaping ((CollectionViewCell, IndexPath) -> ())) -> Self { + willDisplayCellHandler = handler + + return self + } + + @discardableResult + func didEndDisplayingCellFor(_ handler: @escaping ((CollectionViewCell, IndexPath) -> ())) -> Self { + didEndDisplayingCell = handler + + return self + } + + @discardableResult + func transitionLayoutFor(_ handler: @escaping (_ old: UICollectionViewLayout, _ new: UICollectionViewLayout) -> UICollectionViewTransitionLayout) -> Self { + transitionLayoutForHandler = handler + + return self + } + + @discardableResult + func targetContentOffsetFor(_ handler: @escaping (_ proposed: CGPoint) -> CGPoint) -> Self { + targetContentOffsetForHandler = handler + + return self + } + + @discardableResult + func targetIndexPathForMove(_ handler: @escaping (_ from: IndexPath, _ to: IndexPath) -> IndexPath) -> Self { + targetIndexPathForMoveHandler = handler + + return self + } + + @discardableResult + func canFocusItemAt(_ handler: @escaping (IndexPath) -> Bool) -> Self { + canFocusItemAtHandler = handler + + return self + } +} + +// MARK: - Auxiliary Delegation +@available(iOS 11, *) +extension CollectionView: UICollectionViewDelegate { + public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + return shouldSelectItemAtHandler?(indexPath) ?? true + } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + didSelectItemAtHandler?(indexPath) + } + + public func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { + shouldDeselectItemAtHandler?(indexPath) ?? true + } + + public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + didDeselectItemAtHandler?(indexPath) + } + + public func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool { + shouldBeginMultipleSelectionInteractionAtHandler?(indexPath) ?? true + } + + public func collectionView(_ collectionView: UICollectionView, didBeginMultipleSelectionInteractionAt indexPath: IndexPath) { + didBeginMultipleSelectionInteractionAtHandler?(indexPath) + } + + public func collectionViewDidEndMultipleSelectionInteraction(_ collectionView: UICollectionView) { + didEndMultipleSelectionInteractionHandler?(collectionView) + } + + public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { + shouldHighlightItemAtHandler?(indexPath) ?? true + } + + public func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) { + didHighlightItemAtHandler?(indexPath) + } + + public func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) { + didUnhighlightItemAtHandler?(indexPath) + } + + public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + willDisplayCellHandler?(cell as! CollectionViewCell, indexPath) + } + + public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + didEndDisplayingCell?(cell as! CollectionViewCell, indexPath) + } + + public func collectionView(_ collectionView: UICollectionView, transitionLayoutForOldLayout fromLayout: UICollectionViewLayout, newLayout toLayout: UICollectionViewLayout) -> UICollectionViewTransitionLayout { + transitionLayoutForHandler?(fromLayout, toLayout) ?? + UICollectionViewTransitionLayout(currentLayout: fromLayout, nextLayout: toLayout) + } + + public func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { + targetContentOffsetForHandler?(proposedContentOffset) ?? proposedContentOffset + } + + public func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath { + targetIndexPathForMoveHandler?(originalIndexPath, proposedIndexPath) ?? proposedIndexPath + } + + public func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool { + canFocusItemAtHandler?(indexPath) ?? true + } +} + +// MARK: - Layout modifiers +@available(iOS 11, *) +public extension CollectionView { + @discardableResult + func layoutSizeForItem(_ handler: @escaping ((UICollectionViewLayout, IndexPath) -> CGSize)) -> Self { + layoutSizeForItemHandler = handler + + return self + } + + @discardableResult + func sectionInsets(shouldUpdate: Bool = true, insets: [UIEdgeInsets]) -> Self { + sectionsInsets = insets + + if shouldUpdate { + layoutIfNeeded() + } + + return self + } + + @discardableResult + func minimumLineSpacing(shouldUpdate: Bool = true, forSections spacings: [CGFloat]) -> Self { + minimumLineSpacingForSections = spacings + + if shouldUpdate { + layoutIfNeeded() + } + + return self + } + + @discardableResult + func minimumInteritemSpacing(shouldUpdate: Bool = true, forSections spacings: [CGFloat]) -> Self { + minimumInteritemSpacingForSections = spacings + + if shouldUpdate { + layoutIfNeeded() + } + + return self + } + + @discardableResult + func headerSize(shouldUpdate: Bool = true, forSections sizes: [CGSize]) -> Self { + headerSizesForSections = sizes + + if shouldUpdate { + layoutIfNeeded() + } + + return self + } + + @discardableResult + func footerSize(shouldUpdate: Bool = true, forSections sizes: [CGSize]) -> Self { + footerSizeForSections = sizes + + if shouldUpdate { + layoutIfNeeded() + } + + return self + } +} + +// MARK: - Collection Flow Layout Delegate +@available(iOS 11, *) +extension CollectionView: UICollectionViewDelegateFlowLayout { + /// Returns value based on size of given array's count and `numberOfSections` integer. + /// - Parameters: + /// - array: Array of objects containing value that can be returned + /// - section: section which will be used to get value from array + /// - Returns: First value of array if array's count isn't equal `numberOfSections`, value from given index if array's count is equal to `numberOfSections` and nil if array is nil. + func returnValidValue(for array: [T]?, section: Int) -> T? { + if let array = array, + array.count != 0 { + + if array.count != numberOfSections || + section > numberOfSections { + let element = array[0] + return element + } else { + let element = array[section] + return element + } + } + + return nil + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + layoutSizeForItemHandler?(collectionViewLayout, indexPath) ?? CGSize(width: 60, height: 60) + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + if let inset = returnValidValue(for: sectionsInsets, section: section) { + return inset + } + + return UIEdgeInsets() + } + + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + if let spacing = returnValidValue(for: minimumLineSpacingForSections, section: section) { + return spacing + } + + return 10 + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + if let spacing = returnValidValue(for: minimumInteritemSpacingForSections, section: section) { + return spacing + } + + return 5 + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + if let size = returnValidValue(for: headerSizesForSections, section: section) { + return size + } + + return .zero + } + + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + if let size = returnValidValue(for: footerSizeForSections, section: section) { + return size + } + + return .zero + } +} diff --git a/Sources/SwiftUIKit/Views/ContainerView.swift b/Sources/SwiftUIKit/Views/ContainerView.swift new file mode 100644 index 0000000..4fc0420 --- /dev/null +++ b/Sources/SwiftUIKit/Views/ContainerView.swift @@ -0,0 +1,43 @@ +// +// ContainerView.swift +// SwiftUIKit +// +// Created by Zach Eriksen on 5/17/20. +// + +import UIKit + +@available(iOS 9.0, *) +public class ContainerView: UIView { + private weak var parentViewController: UIViewController? + var viewController: UIViewController? + + public init(parent: UIViewController, child: () -> UIViewController) { + parentViewController = parent + viewController = child() + super.init(frame: .zero) + + embedViewController() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +@available(iOS 9.0, *) +private extension ContainerView { + func embedViewController() { + guard let parent = parentViewController, + let child = viewController else { + return + } + + parent.addChild(child) + child.didMove(toParent: parent) + + embed { + child.view + } + } +} diff --git a/Sources/SwiftUIKit/Views/Table.swift b/Sources/SwiftUIKit/Views/List.swift similarity index 95% rename from Sources/SwiftUIKit/Views/Table.swift rename to Sources/SwiftUIKit/Views/List.swift index 285f768..63172f9 100644 --- a/Sources/SwiftUIKit/Views/Table.swift +++ b/Sources/SwiftUIKit/Views/List.swift @@ -8,7 +8,7 @@ import UIKit @available(iOS 9.0, *) -public class Table: UITableView { +public class List: UITableView { private var data: [UIView] private var defaultCellHeight: Float? private var estimatedCellHeight: Float? @@ -38,7 +38,7 @@ public class Table: UITableView { } @available(iOS 9.0, *) -public extension Table { +public extension List { @discardableResult func didSelectHandler(_ action: @escaping (UIView) -> Void) -> Self { self.didSelectHandler = action @@ -55,7 +55,7 @@ public extension Table { } @available(iOS 9.0, *) -extension Table: UITableViewDataSource { +extension List: UITableViewDataSource { public func numberOfSections(in tableView: UITableView) -> Int { return 1 } @@ -96,7 +96,7 @@ extension Table: UITableViewDataSource { } @available(iOS 9.0, *) -extension Table: UITableViewDelegate { +extension List: UITableViewDelegate { public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { didSelectHandler?(data[indexPath.row]) } diff --git a/Sources/SwiftUIKit/Views/Map.swift b/Sources/SwiftUIKit/Views/Map.swift new file mode 100644 index 0000000..d2afd5b --- /dev/null +++ b/Sources/SwiftUIKit/Views/Map.swift @@ -0,0 +1,496 @@ +// +// Map.swift +// SwiftUIKit +// +// Created by Oskar on 12/04/2020. +// + +import Foundation +import MapKit + +public struct MapPoint { + public let latitude: Double + public let longitude: Double + public let title: String + public let subtitle: String + + public init(latitude: Double, longitude: Double, title: String, subtitle: String) { + self.latitude = latitude + self.longitude = longitude + self.title = title + self.subtitle = subtitle + } +} + +public class Map: MKMapView { + + fileprivate var initialCoordinates: CLLocationCoordinate2D + + fileprivate lazy var onFinishLoadingHandler: ((MKMapView) -> ())? = nil + + fileprivate lazy var afterRegionChangeHandler: ((MKMapView) -> ())? = nil + + fileprivate lazy var beforeRegionChangeHandler: ((MKMapView) -> ())? = nil + + fileprivate lazy var annotationViewConfigurationHandler: ((MKAnnotationView?, MKAnnotation) -> (MKAnnotationView?))? = nil + + fileprivate lazy var onAccessoryTapHandler: ((MKMapView, MKAnnotationView, UIControl) -> ())? = nil + + fileprivate lazy var onAnnotationViewStateChangeHandler: ((MKMapView, MKAnnotationView, MKAnnotationView.DragState, MKAnnotationView.DragState) -> ())? = nil + + fileprivate lazy var onAnnotationSelectHandler: ((MKMapView, MKAnnotationView) -> ())? = nil + + fileprivate lazy var onAnnotationDeselectHandler: ((MKMapView, MKAnnotationView) -> ())? = nil + + fileprivate lazy var annotationViewIdentifier: String? = nil + + public init(lat latitude: Double, + lon longitude: Double, + points: (() -> [MapPoint])? = nil) { + let coordinates = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + + initialCoordinates = coordinates + + super.init(frame: .zero) + + let span = MKCoordinateSpan(latitudeDelta: region.span.latitudeDelta / 2, + longitudeDelta: region.span.longitudeDelta / 2) + + let region = MKCoordinateRegion(center: coordinates, span: span) + setRegion(region, animated: true) + + if let points = points { + add(points: points()) + } + + self.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Initializers +public extension Map { + convenience init(region: MKCoordinateRegion, + points: (() -> [MapPoint])? = nil) { + self.init(lat: region.center.latitude, + lon: region.center.longitude, + points: points) + } +} + +// MARK: - Accessing Map Properties +public extension Map { + @discardableResult + func type(_ type: MKMapType) -> Self { + mapType = type + + return self + } + + @discardableResult + func zoomEnabled(_ value: Bool = true) -> Self { + isZoomEnabled = value + + return self + } + + @discardableResult + func scrollEnabled(_ value: Bool = true) -> Self { + isScrollEnabled = value + + return self + } + + @discardableResult + func pitchEnabled(_ value: Bool = true) -> Self { + isPitchEnabled = value + + return self + } + + @discardableResult + func rotateEnabled(_ value: Bool = true) -> Self { + isRotateEnabled = value + + return self + } + + /// Note: If delegate isn't its own class, modifiers based on delegate's methods will do nothing. + @discardableResult + func delegate(_ delegate: MKMapViewDelegate?) -> Self { + self.delegate = delegate ?? self + + return self + } +} + +// MARK: - Manipulating the Visible Portion of the Map +public extension Map { + @discardableResult + func zoom(_ multiplier: Double) -> Self { + let _center = initialCoordinates + let _span = MKCoordinateSpan(latitudeDelta: region.span.latitudeDelta / multiplier / 10, + longitudeDelta: region.span.longitudeDelta / multiplier / 10) + let _region = MKCoordinateRegion(center: _center, span: _span) + + setRegion(_region, animated: false) + return self + } + + @discardableResult + func visible(rect: MKMapRect, + animate: Bool = true, + edgePadding: UIEdgeInsets? = nil + ) -> Self { + if let padding = edgePadding { + setVisibleMapRect(rect, edgePadding: padding, animated: animate) + } else { + setVisibleMapRect(rect, animated: animate) + } + + return self + } + + /// Changes coordinates and span. + @discardableResult + func move(to region: MKCoordinateRegion, animate: Bool = true) -> Self { + initialCoordinates = region.center + setRegion(region, animated: animate) + + return self + } + + + /// Changes only coordinates. + @discardableResult + func move(to coordinates: CLLocationCoordinate2D, animate: Bool = true) -> Self { + let _region = MKCoordinateRegion(center: coordinates, span: region.span) + initialCoordinates = coordinates + setRegion(_region, animated: animate) + + return self + } + + @discardableResult + func center(_ center: CLLocationCoordinate2D, animated: Bool = true) -> Self { + setCenter(center, animated: animated) + + return self + } + + @discardableResult + func show(annotations: [MKAnnotation], animated: Bool = true) -> Self { + super.showAnnotations(annotations, animated: animated) + + return self + } + + @discardableResult + func show(annotations: MKAnnotation..., animated: Bool = true) -> Self { + super.showAnnotations(annotations, animated: animated) + + return self + } +} + +// MARK: - Constraining the Map View + +@available(iOS 13.0, *) +public extension Map { + @discardableResult + func camera(boundary: MKMapView.CameraBoundary?, animated: Bool = true) -> Self { + setCameraBoundary(boundary, animated: animated) + + return self + } + + @discardableResult + func set(cameraZoomRange: MKMapView.CameraZoomRange?, animated: Bool) -> Self { + super.setCameraZoomRange(cameraZoomRange, animated: animated) + + return self + } +} + +// MARK: - Configuring the Map's Appearance +public extension Map { + @discardableResult + func camera(_ camera: MKMapCamera, animated: Bool = true) -> Self { + setCamera(camera, animated: animated) + + return self + } + + @discardableResult + func showBuildings(_ bool: Bool) -> Self { + showsBuildings = bool + + return self + } +} + +@available(iOS 13.0, *) +public extension Map { + @discardableResult + func showCompass(_ bool: Bool) -> Self { + showsCompass = bool + + return self + } + + @discardableResult + func showScale(_ bool: Bool) -> Self { + showsScale = bool + + return self + } + + @discardableResult + func showTraffic(_ bool: Bool) -> Self { + showsTraffic = bool + + return self + } + + @discardableResult + func pointOfInterestFilter(filter: MKPointOfInterestFilter?) -> Self { + pointOfInterestFilter = filter + + return self + } +} + +// MARK: - Displaying the User's Location +public extension Map { + @discardableResult + func showUserLocation(_ bool: Bool) -> Self { + showsUserLocation = bool + + return self + } + + @discardableResult + func user(trackingMode: MKUserTrackingMode, animated: Bool = true) -> Self { + setUserTrackingMode(trackingMode, animated: animated) + + return self + } +} + +// MARK: - Managing Annotation Selections +public extension Map { + @discardableResult + func select(annotation: MKAnnotation, animated: Bool = true) -> Self { + selectAnnotation(annotation, animated: animated) + + return self + } + + @discardableResult + func deselect(annotation: MKAnnotation, animated: Bool = true) -> Self { + deselectAnnotation(annotation, animated: animated) + + return self + } +} + +// MARK: - Annotating the Map +public extension Map { + @discardableResult + func remove(annotation: MKAnnotation) -> Self { + removeAnnotation(annotation) + + return self + } + + @discardableResult + func remove(annotations: [MKAnnotation]) -> Self { + removeAnnotations(annotations) + + return self + } + + @discardableResult + func add(annotation: MKAnnotation) -> Self { + addAnnotation(annotation) + + return self + } + + @discardableResult + func add(point: MapPoint) -> Self { + DispatchQueue.global().async { + let annotation = MKPointAnnotation() + + annotation.coordinate = CLLocationCoordinate2D(latitude: point.latitude, + longitude: point.longitude) + annotation.title = point.title + annotation.subtitle = point.subtitle + + DispatchQueue.main.async { + self.addAnnotation(annotation) + } + } + + return self + } + + @discardableResult + func add(annotations: [MKAnnotation]) -> Self { + addAnnotations(annotations) + + return self + } + + @discardableResult + func add(points: [MapPoint]) -> Self { + for point in points { + add(point: point) + } + + return self + } +} + +// MARK: - Creating Annotation Views +@available(iOS 11.0, *) +public extension Map { + @discardableResult + func register(classes: [String: AnyClass?]) -> Self { + for (identifier, annotationClass) in classes { + register(annotationClass, forAnnotationViewWithReuseIdentifier: identifier) + } + + return self + } +} + +// MARK: - Adjusting Map Regions and Rectangles +public extension Map { + @discardableResult + func fitTo(region: MKCoordinateRegion) -> Self { + self.region = regionThatFits(region) + + return self + } + + @discardableResult + func fitTo(rect: MKMapRect, edgePadding: UIEdgeInsets? = nil) -> Self { + if let edgePadding = edgePadding { + mapRectThatFits(rect, edgePadding: edgePadding) + } else { + mapRectThatFits(rect) + } + + return self + } +} + +// MARK: - Delegate wrappers +// If delegate isn't its own class, methods below will not execute. +public extension Map { + @discardableResult + func onFinishLoading(_ handler: @escaping (MKMapView) -> ()) -> Self { + guard delegate === self else { return self } + onFinishLoadingHandler = handler + + return self + } + + @discardableResult + func afterRegionChange(_ handler: @escaping (MKMapView) -> ()) -> Self { + guard delegate === self else { return self } + afterRegionChangeHandler = handler + + return self + } + + @discardableResult + func beforeRegionChange(_ handler: @escaping (MKMapView) -> ()) -> Self { + guard delegate === self else { return self } + beforeRegionChangeHandler = handler + + return self + } + + @discardableResult + func configure(identifier: String?, _ annotationView: @escaping ((MKAnnotationView?, MKAnnotation) -> (MKAnnotationView?))) -> Self { + guard delegate === self else { return self } + annotationViewIdentifier = identifier + annotationViewConfigurationHandler = annotationView + + return self + } + + @discardableResult + func onAccessoryTap(_ handler: @escaping (MKMapView, MKAnnotationView, UIControl) -> ()) -> Self { + onAccessoryTapHandler = handler + + return self + } + + @discardableResult + func onAnnotationViewStateChange(_ handler: @escaping ((MKMapView, MKAnnotationView, MKAnnotationView.DragState, MKAnnotationView.DragState) -> ())) -> Self { + onAnnotationViewStateChangeHandler = handler + + return self + } + + @discardableResult + func onAnnotationSelect(_ handler: @escaping ((MKMapView, MKAnnotationView) -> ())) -> Self { + onAnnotationSelectHandler = handler + + return self + } + + @discardableResult + func onAnnotationDeselect(_ handler: @escaping ((MKMapView, MKAnnotationView) -> ())) -> Self { + onAnnotationDeselectHandler = handler + + return self + } +} + +// MARK: - Delegation +extension Map: MKMapViewDelegate { + public func mapViewDidFinishLoadingMap(_ mapView: MKMapView) { + onFinishLoadingHandler?(mapView) + } + + public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + afterRegionChangeHandler?(mapView) + } + + public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + beforeRegionChangeHandler?(mapView) + } + + public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + if let identifier = annotationViewIdentifier { + let annotationView = dequeueReusableAnnotationView(withIdentifier: identifier) + + return annotationViewConfigurationHandler?(annotationView, annotation) + } + + return annotationViewConfigurationHandler?(nil, annotation) + } + + public func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { + onAccessoryTapHandler?(mapView, view, control) + } + + public func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, didChange newState: MKAnnotationView.DragState, fromOldState oldState: MKAnnotationView.DragState) { + onAnnotationViewStateChangeHandler?(mapView, view, newState, oldState) + } + + public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + onAnnotationSelectHandler?(mapView, view) + } + + public func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { + onAnnotationDeselectHandler?(mapView, view) + } +} diff --git a/Sources/SwiftUIKit/Views/Slider.swift b/Sources/SwiftUIKit/Views/Slider.swift index 31ba233..7feafa1 100644 --- a/Sources/SwiftUIKit/Views/Slider.swift +++ b/Sources/SwiftUIKit/Views/Slider.swift @@ -18,9 +18,9 @@ public class Slider: UISlider { super.init(frame: .zero) - self.value = value self.minimumValue = from self.maximumValue = to + self.value = value self.valueChangedHandler = valueChangedHandler addTarget(self, action: #selector(handleValueChanged), for: .valueChanged) diff --git a/Sources/SwiftUIKit/Views/TableView.swift b/Sources/SwiftUIKit/Views/TableView.swift index 4ddea84..834a7ae 100644 --- a/Sources/SwiftUIKit/Views/TableView.swift +++ b/Sources/SwiftUIKit/Views/TableView.swift @@ -7,33 +7,35 @@ import UIKit -@available(iOS 9.0, *) +@available(iOS 11.0, *) public protocol CellDisplayable { var cellID: String { get } } -@available(iOS 9.0, *) -public protocol DataConfigurable: UITableViewCell { +@available(iOS 11.0, *) +public protocol DataIdentifiable { static var ID: String { get } } -@available(iOS 9.0, *) -public protocol CellUpdatable: UITableViewCell { +@available(iOS 11.0, *) +public protocol CellUpdatable { func update(forData data: CellDisplayable) } -@available(iOS 9.0, *) -public protocol CellConfigurable: UITableViewCell { +@available(iOS 11.0, *) +public protocol CellConfigurable { func configure(forData data: CellDisplayable) } -@available(iOS 9.0, *) -public typealias TableViewCell = DataConfigurable & CellConfigurable & CellUpdatable +@available(iOS 11.0, *) +public typealias TableViewCell = DataIdentifiable & CellConfigurable & CellUpdatable & UITableViewCell public typealias TableHeaderFooterViewHandler = (Int) -> UIView? public typealias TableHeaderFooterTitleHandler = (Int) -> String? +public typealias TableDidSelectIndexPathHandler = (IndexPath) -> Void +public typealias TableHighlightIndexPathHandler = (IndexPath) -> Bool -@available(iOS 9.0, *) +@available(iOS 11.0, *) public class TableView: UITableView { public var data: [[CellDisplayable]] @@ -41,6 +43,26 @@ public class TableView: UITableView { fileprivate var footerViewForSection: TableHeaderFooterViewHandler? fileprivate var headerTitleForSection: TableHeaderFooterTitleHandler? fileprivate var footerTitleForSection: TableHeaderFooterTitleHandler? + fileprivate var didSelectRowAtIndexPath: TableDidSelectIndexPathHandler? + fileprivate var shouldHighlightRowAtIndexPath: TableHighlightIndexPathHandler? + fileprivate var canEditRowAtIndexPath: ((IndexPath) -> Bool)? + fileprivate var canMoveRowAtIndexPath: ((IndexPath) -> Bool)? + fileprivate var canFocusRowAtIndexPath: ((IndexPath) -> Bool)? + fileprivate var indentationLevelForRowAtIndexPath: ((IndexPath) -> Int)? + fileprivate var shouldIndentWhileEditingRowAtIndexPath: ((IndexPath) -> Bool)? + fileprivate var shouldShowMenuForRowAtIndexPath: ((IndexPath) -> Bool)? + fileprivate var editingStyleForRowAtIndexPath: ((IndexPath) -> UITableViewCell.EditingStyle)? + fileprivate var titleForDeleteConfirmationButtonForRowAtIndexPath: ((IndexPath) -> String)? + fileprivate var editActionsForRowAtIndexPath: ((IndexPath) -> [UITableViewRowAction])? + fileprivate var commitEditingStyleForRowAtIndexPath: ((UITableViewCell.EditingStyle, IndexPath) -> Void)? + fileprivate var didDeselectRowAtIndexPath: ((IndexPath) -> Void)? + fileprivate var willBeginEditingRowAtIndexPath: ((IndexPath) -> Void)? + fileprivate var didEndEditingRowAtIndexPath: ((IndexPath?) -> Void)? + fileprivate var didHighlightRowAtIndexPath: ((IndexPath) -> Void)? + fileprivate var didUnhighlightRowAtIndexPath: ((IndexPath) -> Void)? + fileprivate var moveRowAtSourceIndexPathToDestinationIndexPath: ((IndexPath, IndexPath) -> Void)? + fileprivate var leadingSwipeActionsConfigurationForRowAtIndexPath: ((IndexPath) -> UISwipeActionsConfiguration)? + fileprivate var trailingSwipeActionsConfigurationForRowAtIndexPath: ((IndexPath) -> UISwipeActionsConfiguration)? public init(initalData: [[CellDisplayable]] = [[CellDisplayable]](), style: UITableView.Style = .plain) { @@ -56,7 +78,7 @@ public class TableView: UITableView { } } -@available(iOS 9.0, *) +@available(iOS 11.0, *) public extension TableView { @discardableResult func update(shouldReloadData: Bool = false, @@ -83,12 +105,12 @@ public extension TableView { } } -@available(iOS 9.0, *) +@available(iOS 11.0, *) extension TableView: UITableViewDelegate { } -@available(iOS 9.0, *) +@available(iOS 11.0, *) extension TableView: UITableViewDataSource { func sections() -> Int { data.count @@ -106,6 +128,10 @@ extension TableView: UITableViewDataSource { rows(forSection: section) } + public func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int { + indentationLevelForRowAtIndexPath?(indexPath) ?? 0 + } + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellData = data[indexPath.section][indexPath.row] @@ -126,6 +152,8 @@ extension TableView: UITableViewDataSource { return cell } + // MARK: HeaderForSection + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { headerViewForSection?(section) } @@ -134,6 +162,8 @@ extension TableView: UITableViewDataSource { headerTitleForSection?(section) } + // MARK: FooterForSection + public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { footerViewForSection?(section) } @@ -141,9 +171,95 @@ extension TableView: UITableViewDataSource { public func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { footerTitleForSection?(section) } + + // MARK: CanRowAt + + public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + canEditRowAtIndexPath?(indexPath) ?? false + } + + public func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { + canMoveRowAtIndexPath?(indexPath) ?? false + } + + public func tableView(_ tableView: UITableView, canFocusRowAt indexPath: IndexPath) -> Bool { + canFocusRowAtIndexPath?(indexPath) ?? false + } + + // MARK: ShouldRowAt + + public func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + shouldHighlightRowAtIndexPath?(indexPath) ?? true + } + + public func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool { + shouldIndentWhileEditingRowAtIndexPath?(indexPath) ?? false + } + + public func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { + shouldShowMenuForRowAtIndexPath?(indexPath) ?? false + } + + // MARK: Editing + + public func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + editingStyleForRowAtIndexPath?(indexPath) ?? .none + } + + public func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? { + titleForDeleteConfirmationButtonForRowAtIndexPath?(indexPath) + } + + public func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + editActionsForRowAtIndexPath?(indexPath) + } + + // MARK: Actions + + public func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + commitEditingStyleForRowAtIndexPath?(editingStyle, indexPath) + } + + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + didSelectRowAtIndexPath?(indexPath) + } + + public func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + didDeselectRowAtIndexPath?(indexPath) + } + + public func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { + willBeginEditingRowAtIndexPath?(indexPath) + } + + public func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { + didEndEditingRowAtIndexPath?(indexPath) + } + + public func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) { + didHighlightRowAtIndexPath?(indexPath) + } + + public func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) { + didUnhighlightRowAtIndexPath?(indexPath) + } + + public func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + moveRowAtSourceIndexPathToDestinationIndexPath?(sourceIndexPath, destinationIndexPath) + } + + @available(iOS 11.0, *) + public func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + leadingSwipeActionsConfigurationForRowAtIndexPath?(indexPath) ?? .none + } + + @available(iOS 11.0, *) + public func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + trailingSwipeActionsConfigurationForRowAtIndexPath?(indexPath) ?? .none + } } -@available(iOS 9.0, *) +@available(iOS 11.0, *) public extension TableView { @discardableResult func set(dataSource: UITableViewDataSource) -> Self { @@ -195,6 +311,144 @@ public extension TableView { return self } + + @discardableResult + func indentationLevelForRowAtIndexPath(_ handler: @escaping (IndexPath) -> Int) -> Self { + indentationLevelForRowAtIndexPath = handler + + return self + } + + @discardableResult + func canEditRowAtIndexPath(_ handler: @escaping (IndexPath) -> Bool) -> Self { + canEditRowAtIndexPath = handler + + return self + } + + @discardableResult + func canMoveRowAtIndexPath(_ handler: @escaping (IndexPath) -> Bool) -> Self { + canMoveRowAtIndexPath = handler + + return self + } + + @discardableResult + func canFocusRowAtIndexPath(_ handler: @escaping (IndexPath) -> Bool) -> Self { + canFocusRowAtIndexPath = handler + + return self + } + + @discardableResult + func shouldHighlightRow(_ handler: @escaping TableHighlightIndexPathHandler) -> Self { + shouldHighlightRowAtIndexPath = handler + + return self + } + + @discardableResult + func shouldIndentWhileEditingRowAtIndexPath(_ handler: @escaping (IndexPath) -> Bool) -> Self { + shouldIndentWhileEditingRowAtIndexPath = handler + + return self + } + + @discardableResult + func shouldShowMenuForRowAtIndexPath(_ handler: @escaping (IndexPath) -> Bool) -> Self { + shouldShowMenuForRowAtIndexPath = handler + + return self + } + + @discardableResult + func editingStyleForRowAtIndexPath(_ handler: @escaping (IndexPath) -> UITableViewCell.EditingStyle) -> Self { + editingStyleForRowAtIndexPath = handler + + return self + } + + @discardableResult + func titleForDeleteConfirmationButtonForRowAtIndexPath(_ handler: @escaping (IndexPath) -> String) -> Self { + titleForDeleteConfirmationButtonForRowAtIndexPath = handler + + return self + } + + @discardableResult + func editActionsForRowAtIndexPath(_ handler: @escaping (IndexPath) -> [UITableViewRowAction]) -> Self { + editActionsForRowAtIndexPath = handler + + return self + } + + @discardableResult + func commitEditingStyleForRowAtIndexPath(_ handler: @escaping (UITableViewCell.EditingStyle, IndexPath) -> Void) -> Self { + commitEditingStyleForRowAtIndexPath = handler + + return self + } + + @discardableResult + func didSelectRow(_ handler: @escaping TableDidSelectIndexPathHandler) -> Self { + didSelectRowAtIndexPath = handler + + return self + } + + @discardableResult + func didDeselectRowAtIndexPath(_ handler: @escaping (IndexPath) -> Void) -> Self { + didDeselectRowAtIndexPath = handler + + return self + } + + @discardableResult + func willBeginEditingRowAtIndexPath(_ handler: @escaping (IndexPath) -> Void) -> Self { + willBeginEditingRowAtIndexPath = handler + + return self + } + + @discardableResult + func didEndEditingRowAtIndexPath(_ handler: @escaping (IndexPath?) -> Void) -> Self { + didEndEditingRowAtIndexPath = handler + + return self + } + + @discardableResult + func didHighlightRowAtIndexPath(_ handler: @escaping (IndexPath) -> Void) -> Self { + didHighlightRowAtIndexPath = handler + + return self + } + + @discardableResult + func didUnhighlightRowAtIndexPath(_ handler: @escaping (IndexPath) -> Void) -> Self { + didUnhighlightRowAtIndexPath = handler + + return self + } + + @discardableResult + func moveRowAtSourceIndexPathToDestinationIndexPath(_ handler: @escaping (IndexPath, IndexPath) -> Void) -> Self { + moveRowAtSourceIndexPathToDestinationIndexPath = handler + + return self + } + + @discardableResult + func leadingSwipeActionsConfigurationForRowAtIndexPath(_ handler: @escaping (IndexPath) -> UISwipeActionsConfiguration) -> Self { + leadingSwipeActionsConfigurationForRowAtIndexPath = handler + + return self + } + + @discardableResult + func trailingSwipeActionsConfigurationForRowAtIndexPath(_ handler: @escaping (IndexPath) -> UISwipeActionsConfiguration) -> Self { + trailingSwipeActionsConfigurationForRowAtIndexPath = handler + + return self + } } - - diff --git a/Tests/SwiftUIKitTests/ContainerView/ContainerViewTests.swift b/Tests/SwiftUIKitTests/ContainerView/ContainerViewTests.swift new file mode 100644 index 0000000..1c19569 --- /dev/null +++ b/Tests/SwiftUIKitTests/ContainerView/ContainerViewTests.swift @@ -0,0 +1,90 @@ +// +// ContainerViewTests.swift +// SwiftUIKitTests +// +// Created by Zach Eriksen on 5/17/20. +// + +import XCTest +import UIKit +import SwiftUIKit + +@available(iOS 9.0, *) +class ContainerViewTests: XCTestCase { + + func testBasicContainerView() { + let mainVC = UIViewController() + let someVC = UIViewController() + let containerView = ContainerView(parent: mainVC) { + someVC + } + + XCTAssertEqual(mainVC.children.count, 1) + XCTAssertEqual(someVC.children.count, 0) + XCTAssertEqual(mainVC.view.allSubviews.count, 0) + XCTAssertEqual(someVC.view.allSubviews.count, 0) + XCTAssertEqual(containerView.allSubviews.count, 1) + } + + func testEmbedContainerView() { + let mainVC = UIViewController() + let someVC = UIViewController() + let containerView = ContainerView(parent: mainVC) { + someVC + } + + XCTAssertEqual(mainVC.children.count, 1) + XCTAssertEqual(someVC.children.count, 0) + XCTAssertEqual(mainVC.view.allSubviews.count, 0) + XCTAssertEqual(someVC.view.allSubviews.count, 0) + XCTAssertEqual(containerView.allSubviews.count, 1) + + mainVC.view.embed { + containerView + } + + XCTAssertEqual(mainVC.children.count, 1) + XCTAssertEqual(someVC.children.count, 0) + XCTAssertEqual(mainVC.view.allSubviews.count, 2) + XCTAssertEqual(someVC.view.allSubviews.count, 0) + XCTAssertEqual(containerView.allSubviews.count, 1) + } + + func testChildVCEmbedContainerView() { + let mainVC = UIViewController() + let someVC = UIViewController() + let containerView = ContainerView(parent: mainVC) { + someVC + } + + XCTAssertEqual(mainVC.children.count, 1) + XCTAssertEqual(someVC.children.count, 0) + XCTAssertEqual(mainVC.view.allSubviews.count, 0) + XCTAssertEqual(someVC.view.allSubviews.count, 0) + XCTAssertEqual(containerView.allSubviews.count, 1) + + mainVC.view.embed { + containerView + } + + XCTAssertEqual(mainVC.children.count, 1) + XCTAssertEqual(someVC.children.count, 0) + XCTAssertEqual(mainVC.view.allSubviews.count, 2) + XCTAssertEqual(someVC.view.allSubviews.count, 0) + XCTAssertEqual(containerView.allSubviews.count, 1) + + someVC.view.embed { + VStack { + [ + Label("Something") + ] + } + } + + XCTAssertEqual(mainVC.children.count, 1) + XCTAssertEqual(someVC.children.count, 0) + XCTAssertEqual(mainVC.view.allSubviews.count, 5) + XCTAssertEqual(someVC.view.allSubviews.count, 3) + XCTAssertEqual(containerView.allSubviews.count, 4) + } +} diff --git a/Tests/SwiftUIKitTests/Core/BasicSwiftUIKitTests.swift b/Tests/SwiftUIKitTests/Core/BasicSwiftUIKitTests.swift index e9b97a5..655dc4f 100644 --- a/Tests/SwiftUIKitTests/Core/BasicSwiftUIKitTests.swift +++ b/Tests/SwiftUIKitTests/Core/BasicSwiftUIKitTests.swift @@ -9,7 +9,7 @@ import Foundation import XCTest @testable import SwiftUIKit -@available(iOS 9.0, *) +@available(iOS 10.0, *) final class BasicSwiftUIKitTests: XCTestCase { func testDefaultView() { @@ -31,9 +31,26 @@ final class BasicSwiftUIKitTests: XCTestCase { viewToEmbed } + let leadingConstraint = view.leadingConstraints.first + let bottomConstraint = view.bottomConstraints.first + let trailingConstraint = view.trailingConstraints.first + let topConstraint = view.topConstraints.first + XCTAssertNil(view.backgroundColor) XCTAssert(view.allSubviews.count == 1) XCTAssert(view.constraints.count == 4) + XCTAssertEqual(leadingConstraint?.constant, 0) + XCTAssertEqual(bottomConstraint?.constant, 0) + XCTAssertEqual(trailingConstraint?.constant, 0) + XCTAssertEqual(topConstraint?.constant, 0) + + view.update(padding: 16) + + XCTAssert(view.constraints.count == 4) + XCTAssertEqual(leadingConstraint?.constant, 16) + XCTAssertEqual(bottomConstraint?.constant, -16) + XCTAssertEqual(trailingConstraint?.constant, -16) + XCTAssertEqual(topConstraint?.constant, 16) } func testEmbedView_WithOnePadding() { @@ -48,9 +65,18 @@ final class BasicSwiftUIKitTests: XCTestCase { viewToEmbed } + let constraint = view.leadingConstraints.first + XCTAssertNil(view.backgroundColor) XCTAssert(view.allSubviews.count == 1) XCTAssert(view.constraints.count == 1) + XCTAssertEqual(constraint?.constant, 16) + + view.update(padding: .leading(8)) + view.update(padding: .trailing(16)) + + XCTAssertEqual(constraint?.constant, 8) + XCTAssertEqual(view.constraints.count, 1) } func testEmbedView_WithTwoPadding() { @@ -66,9 +92,21 @@ final class BasicSwiftUIKitTests: XCTestCase { viewToEmbed } + let leadingConstraint = view.leadingConstraints.first + let bottomConstraint = view.bottomConstraints.first + XCTAssertNil(view.backgroundColor) XCTAssert(view.allSubviews.count == 1) XCTAssert(view.constraints.count == 2) + XCTAssertEqual(leadingConstraint?.constant, 16) + XCTAssertEqual(bottomConstraint?.constant, -16) + + view.update(padding: .leading(8)) + view.update(padding: .bottom(32)) + + XCTAssert(view.constraints.count == 2) + XCTAssertEqual(leadingConstraint?.constant, 8) + XCTAssertEqual(bottomConstraint?.constant, -32) } func testEmbedView_WithThreePadding() { @@ -85,9 +123,23 @@ final class BasicSwiftUIKitTests: XCTestCase { viewToEmbed } + let leadingConstraint = view.leadingConstraints.first + let bottomConstraint = view.bottomConstraints.first + let trailingConstraint = view.trailingConstraints.first + XCTAssertNil(view.backgroundColor) XCTAssert(view.allSubviews.count == 1) XCTAssert(view.constraints.count == 3) + XCTAssertEqual(leadingConstraint?.constant, 16) + XCTAssertEqual(bottomConstraint?.constant, -16) + XCTAssertEqual(trailingConstraint?.constant, -16) + + view.update(padding: [.leading(32), .trailing(32), .bottom(32)]) + + XCTAssert(view.constraints.count == 3) + XCTAssertEqual(leadingConstraint?.constant, 32) + XCTAssertEqual(bottomConstraint?.constant, -32) + XCTAssertEqual(trailingConstraint?.constant, -32) } func testEmbedView_WithAllPadding() { @@ -105,9 +157,26 @@ final class BasicSwiftUIKitTests: XCTestCase { viewToEmbed } + let leadingConstraint = view.leadingConstraints.first + let bottomConstraint = view.bottomConstraints.first + let trailingConstraint = view.trailingConstraints.first + let topConstraint = view.topConstraints.first + XCTAssertNil(view.backgroundColor) XCTAssert(view.allSubviews.count == 1) XCTAssert(view.constraints.count == 4) + XCTAssertEqual(leadingConstraint?.constant, 16) + XCTAssertEqual(bottomConstraint?.constant, -16) + XCTAssertEqual(trailingConstraint?.constant, -16) + XCTAssertEqual(topConstraint?.constant, 16) + + view.update(padding: 32) + + XCTAssert(view.constraints.count == 4) + XCTAssertEqual(leadingConstraint?.constant, 32) + XCTAssertEqual(bottomConstraint?.constant, -32) + XCTAssertEqual(trailingConstraint?.constant, -32) + XCTAssertEqual(topConstraint?.constant, 32) } func testEmbedViews() { @@ -251,11 +320,14 @@ final class BasicSwiftUIKitTests: XCTestCase { } } + // Will fail unless... + // === (iOS >= 13) === XCTAssert(switchView.allSubviews.count == 8, "switchView.allSubviews.count == \(switchView.allSubviews.count)") XCTAssert(uiSwitchView.allSubviews.count == 8, "uiSwitchView.allSubviews.count == \(uiSwitchView.allSubviews.count)") - XCTAssert(view.allSubviews.count == 12, "view.allSubviews.count == \(view.allSubviews.count)") XCTAssert(otherView.allSubviews.count == 12, "otherView.allSubviews.count == \(otherView.allSubviews.count)") + // === (End) === + XCTAssert(viewWithoutSwitch.allSubviews.count == 3, "viewWithoutSwitch.allSubviews.count == \(viewWithoutSwitch.allSubviews.count)") switchView.clear() diff --git a/Tests/SwiftUIKitTests/TableView/TableViewTests.swift b/Tests/SwiftUIKitTests/TableView/TableViewTests.swift index f6fce6d..0cf1c9d 100644 --- a/Tests/SwiftUIKitTests/TableView/TableViewTests.swift +++ b/Tests/SwiftUIKitTests/TableView/TableViewTests.swift @@ -9,7 +9,7 @@ import XCTest import UIKit @testable import SwiftUIKit -@available(iOS 9.0, *) +@available(iOS 11.0, *) class TableViewTests: XCTestCase { func testTableViewNoCells() { @@ -116,7 +116,7 @@ class TableViewTests: XCTestCase { } } -@available(iOS 9.0, *) +@available(iOS 11.0, *) fileprivate class TableTestHelper { struct InfoData { let title: String @@ -124,22 +124,22 @@ fileprivate class TableTestHelper { let bio: String } - class InfoCell: UITableViewCell { + class InfoCell: TableViewCell { let label: Label = Label("") let detailLabel: Label = Label("") let bioLabel: Label = Label("") } } -@available(iOS 9.0, *) +@available(iOS 11.0, *) extension TableTestHelper.InfoData: CellDisplayable { var cellID: String { TableTestHelper.InfoCell.ID } } -@available(iOS 9.0, *) -extension TableTestHelper.InfoCell: TableViewCell { +@available(iOS 11.0, *) +extension TableTestHelper.InfoCell { static var ID: String { "InfoCell" }