Use SwiftUI with UIKit

RSS for tag

Discuss the WWDC22 Session Use SwiftUI with UIKit

Posts under wwdc2022-10072 tag

2 Posts
Sort by:
Post not yet marked as solved
1 Replies
2.5k Views
I have made an UICollectionView in which you can double tap a cell to resize it. I'm using a CompositionalLayout, a DiffableDataSource and the new UIHostingConfiguration hosting a SwiftUI View which depends on an ObservableObject. The resizing is triggered by updating the height property of the ObservableObject. That causes the SwiftUI View to change its frame which leads to the collectionView automatically resizing the cell. The caveat is that it does so immediately without animation only jumping between the old and the new frame of the view. The ideal end-goal would be to be able to add a .animation() modifier to the SwiftUI View that then determines animation for both view and cell. Doing so now without additional setup makes the SwiftUI View animate but not the cell. Is there a way to make the cell (orange) follow the size of the view (green) dynamically? The proper way to manipulate the cell animation (as far as I known) is to override initialLayoutAttributesForAppearingItem() and finalLayoutAttributesForDisappearingItem() but since the cell just changes and doesn't appear/disappear they don't have an effect. One could also think of Auto Layout constraints to archive this but I don’t think they are usable with UIHostingConfiguration? I've also tried: subclassing UICollectionViewCell and overriding apply(_ layoutAttributes: UICollectionViewLayoutAttributes) but it only effects the orange cell-background on initial appearance. to put layout.invalidateLayout() or collectionView.layoutIfNeeded() inside UIView.animate() but it does not seem to have an effect on the size change. Any thoughts, hints, ideas are greatly appreciated ✌️ Cheers! Here is the code I used for the first gif: struct CellContentModel { var height: CGFloat? = 100 } class CellContentController: ObservableObject, Identifiable { let id = UUID() @Published var cellContentModel: CellContentModel init(cellContentModel: CellContentModel) { self.cellContentModel = cellContentModel } } class DataStore { var data: [CellContentController] var dataById: [CellContentController.ID: CellContentController] init(data: [CellContentController]) { self.data = data self.dataById = Dictionary(uniqueKeysWithValues: data.map { ($0.id, $0) } ) } static let testData = [ CellContentController(cellContentModel: CellContentModel()), CellContentController(cellContentModel: CellContentModel(height: 80)), CellContentController(cellContentModel: CellContentModel()) ] } class CollectionViewController: UIViewController { enum Section { case first } var dataStore = DataStore(data: DataStore.testData) private var layout: UICollectionViewCompositionalLayout! private var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource<Section, CellContentController.ID>! override func loadView() { createLayout() createCollectionView() createDataSource() view = collectionView } } // - MARK: Layout extension CollectionViewController { func createLayout() { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50)) let Item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.8), heightDimension: .estimated(300)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [Item]) let section = NSCollectionLayoutSection(group: group) layout = .init(section: section) } } // - MARK: CollectionView extension CollectionViewController { func createCollectionView() { collectionView = .init(frame: .zero, collectionViewLayout: layout) let doubleTapGestureRecognizer = DoubleTapGestureRecognizer() doubleTapGestureRecognizer.doubleTapAction = { [unowned self] touch, _ in let touchLocation = touch.location(in: collectionView) guard let touchedIndexPath = collectionView.indexPathForItem(at: touchLocation) else { return } let touchedItemIdentifier = dataSource.itemIdentifier(for: touchedIndexPath)! dataStore.dataById[touchedItemIdentifier]!.cellContentModel.height = dataStore.dataById[touchedItemIdentifier]!.cellContentModel.height == 100 ? nil : 100 } collectionView.addGestureRecognizer(doubleTapGestureRecognizer) } } // - MARK: DataSource extension CollectionViewController { func createDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, CellContentController.ID>() { cell, indexPath, itemIdentifier in let cellContentController = self.dataStore.dataById[itemIdentifier]! cell.contentConfiguration = UIHostingConfiguration { TextView(cellContentController: cellContentController) } .background(.orange) } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } var initialSnapshot = NSDiffableDataSourceSnapshot<Section, CellContentController.ID>() initialSnapshot.appendSections([Section.first]) initialSnapshot.appendItems(dataStore.data.map{ $0.id }, toSection: Section.first) dataSource.applySnapshotUsingReloadData(initialSnapshot) } } class DoubleTapGestureRecognizer: UITapGestureRecognizer { var doubleTapAction: ((UITouch, UIEvent) -> Void)? override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { if touches.first!.tapCount == 2 { doubleTapAction?(touches.first!, event) } } } struct TextView: View { @StateObject var cellContentController: CellContentController var body: some View { Text(cellContentController.cellContentModel.height?.description ?? "nil") .frame(height: cellContentController.cellContentModel.height, alignment: .top) .background(.green) } }
Posted Last updated
.
Post marked as solved
2 Replies
1.3k Views
I'm implementing a SwiftUI control over a UIView's drawing surface. Often you are doing both at the same time. Problem is that starting with the SwiftUI gesture will block the UIKit's UITouch(s). And yet, it works when you start with a UITouch first and then a SwiftUI gesture. Also need UITouch's force and majorRadius, which isn't available in a SwiftUI gesture (I think). Here's an example: import SwiftUI struct ContentView: View { @GestureState private var touchXY: CGPoint = .zero var body: some View { ZStack { TouchRepresentable() Rectangle() .foregroundColor(.red) .frame(width: 128, height: 128) .gesture(DragGesture(minimumDistance: 0) .updating($touchXY) { (value, touchXY, _) in touchXY = value.location}) .onChange(of: touchXY) { print(String(format:"SwiftUI(%.2g,%.2g)", $0.x,$0.y), terminator: " ") } //.allowsHitTesting(true) no difference } } } struct TouchRepresentable: UIViewRepresentable { typealias Context = UIViewRepresentableContext<TouchRepresentable> public func makeUIView(context: Context) -> TouchView { return TouchView() } public func updateUIView(_ uiView: TouchView, context: Context) {} } class TouchView: UIView, UIGestureRecognizerDelegate { func updateTouches(_ touches: Set<UITouch>, with event: UIEvent?) { for touch in touches { let location = touch.location(in: nil) print(String(format: "UIKit(%g,%g) ", round(location.x), round(location.y)), terminator: " ") } } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { updateTouches(touches, with: event) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { updateTouches(touches, with: event) } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { updateTouches(touches, with: event) } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { updateTouches(touches, with: event) } } Because this is multitouch, you need to test on a real device. Sequence A: finger 1 starts dragging outside of red square (UIKit) finger 2 simultaneously drags inside of red square (SwiftUI) ⟹ Console shows both UIKit and SwiftUI touch positions Sequence B: finger 1 starts dragging inside of red square (SwiftUI) finger 2 simultaneously drags outside of red square (UIKit) ⟹ Console shows only SwiftUI touch positions Would like to get both positions in Sequence B.
Posted
by Muse.
Last updated
.