Kolos65 / InstagramTransition

Replicating Instagram's gesture-driven shared-element transition using UIKit.
20 stars 0 forks source link

blank/black screen, initial view doesn't seem to mount #5

Closed coratype closed 4 months ago

coratype commented 6 months ago

ive been working on a personal swiftui based project for a bit and decided to try to incorporate this just to learn, and im having trouble just getting it going

i created a bare bones/distilled version which is basically a 1:1 version of this repo except with async data fetching. ive attached my feed screen below to give you an idea. i copied over the utils folder and all other relevant files based off yours


import UIKit
import Combine

class FeedScreen: UIViewController {
    private var viewModel = EntryModel()

    // MARK: Constants
    private enum Constants {
        static let numberOfRows = 1
        static let sectionInset: UIEdgeInsets = .init(top: 10, left: 0, bottom: 10, right: 0)
        static let interItemSpacing: CGFloat = 0
        static let lineSpacing: CGFloat = 10
    }

    // MARK: Typealiases
    typealias DataSource = UICollectionViewDiffableDataSource<Int, Entry>
    typealias Snapshot = NSDiffableDataSourceSnapshot<Int, Entry>

    // MARK: UI properties
    private let transitionAnimator = SharedTransitionAnimator()
    private lazy var dataSource = DataSource(collectionView: collectionView, cellProvider: cellProvider)
    private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
    private lazy var layout = UICollectionViewFlowLayout().then {
        $0.sectionInset = Constants.sectionInset
        $0.minimumLineSpacing = Constants.lineSpacing
        $0.minimumInteritemSpacing = Constants.interItemSpacing
    }

    private var cancellables = Set<AnyCancellable>()

    // MARK: Private properties
    private var entries = [Entry]() {
        didSet { updateCollectionView() }
    }

    // MARK: View lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        print("FeedScreen loaded")
        setupUI()
        observeEntries()
        viewModel.fetchEntries()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        navigationController?.delegate = self
        print("FeedScreen appeared")
    }
}

// MARK: - Helpers
extension FeedScreen {
    private func setupUI() {
        setupCollectionView()
    }

//    private func observeEntries() {
//        viewModel.$entries.sink { [weak self] newEntries in
//            self?.updateCollectionView(with: newEntries)
//        }.store(in: &cancellables)
//    }

    private func observeEntries() {
        viewModel.$entries.sink { [weak self] newEntries in
            self?.entries = newEntries
        }.store(in: &cancellables)
    }

    private func updateCollectionView(with entries: [Entry]) {
        var snapshot = Snapshot()
        snapshot.appendSections([0])
        snapshot.appendItems(entries, toSection: 0)
        dataSource.apply(snapshot, animatingDifferences: true)
    }

    private func setupCollectionView() {
        collectionView.then {
            view.addSubview($0)
            $0.register(FeedCell.self)
            $0.dataSource = dataSource
            $0.delegate = self
            $0.delaysContentTouches = false
        }.layout {
            $0.leading == view.leadingAnchor
            $0.trailing == view.trailingAnchor
            $0.top == view.safeAreaLayoutGuide.topAnchor
            $0.bottom == view.bottomAnchor
        }
    }
}

// MARK: - UICollectionView helpers
extension FeedScreen {
    private func updateCollectionView() {
        var snapshot = Snapshot()
        snapshot.appendSections([0])
        snapshot.appendItems(entries, toSection: 0)
        dataSource.apply(snapshot, animatingDifferences: true)
    }

    private var cellProvider: DataSource.CellProvider {
        { collectionView, indexPath, entry in
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FeedCell", for: indexPath) as? FeedCell else {
                fatalError("Cannot create new cell")
            }
            cell.setup(with: entry)
            return cell
        }
    }
}

// MARK: - UICollectionViewDelegate
extension FeedScreen: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let entry = entries[indexPath.item]
        let viewController = EntryScreen(entry: entry)
        navigationController?.pushViewController(viewController, animated: true)
    }
}

// MARK: - UICollectionViewDelegateFlowLayout methods
extension FeedScreen: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize {
        let contentWidth = collectionView.frame.inset(by: Constants.sectionInset).width
        return CGSize(width: contentWidth, height: 100)
    }
}

// MARK: - UINavigationControllerDelegate
extension FeedScreen: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationController.Operation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if fromVC is Self && toVC is EntryScreen || toVC is Self && fromVC is EntryScreen {
            transitionAnimator.transition = (operation == .push ? .push : .pop)
            return transitionAnimator
        }
        return nil
    }
}
coratype commented 6 months ago

if i port my feed screen directly into your repo that i cloned, it works perfectly, it might be something im missing on the project side. is there something specific i have to do? i set the project up as a swiftui project

coratype commented 6 months ago

also i’m not sure if i’m doing something wrong but i’ve set the height constraint to be similar to a card because i’m not transitioning from an image but from a card and it seems like the mask isn’t properly covering the card upon transition (no rounded corners when the detailed screen scales up), is the mask limited to a square ratio or have i missed something?

coratype commented 6 months ago

extension DetailScreen {
    private func setupUI() {
        setupView()
        setupScrollView()
        setupCardView()
    }

    private func setupView() {
        view.backgroundColor = .yellow
        view.addGestureRecognizer(recognizer)
        recognizer.delegate = self
    }

    private func setupScrollView() {
        scrollView.then {
            $0.alwaysBounceVertical = true
            view.addSubview($0)
        }.layout {
            $0.top == view.topAnchor
            $0.leading == view.leadingAnchor
            $0.trailing == view.trailingAnchor
            $0.bottom == view.safeAreaLayoutGuide.bottomAnchor
        }

        contentView.then {
            scrollView.addSubview($0)
            scrollView.fillWith($0)
        }.layout {
            $0.width == scrollView.widthAnchor
            $0.height >= scrollView.heightAnchor
        }
    }

    private func setupCardView() {
        cardView.then {
            contentView.addSubview($0)
            $0.contentMode = .scaleAspectFit
            $0.layer.masksToBounds = true
            cardView.backgroundColor = .red
        }.layout {
            $0.leading == contentView.leadingAnchor
            $0.trailing == contentView.trailingAnchor
            $0.top == contentView.topAnchor + 10
        }

        cardView.heightAnchor.constraint(
            equalTo: cardView.widthAnchor,
            multiplier: 1.42
        ).isActive = true
    }

}
Juhnkerg commented 6 months ago

As far as I know, there is no good implementation in swiftUI. Let's just say that if you put UIKit gestures into swiftUI, they don't flow smoothly, which is the downside of swiftUI. Not all UIKit things can be used well on swiftUI, which is why I gave up swiftUI and switched to UIKit.

coratype commented 4 months ago

for anyone coming across this in the future you can accomplish this in 2 lines of code with SwiftUI as of iOS 18 with the zoom transition

Kolos65 commented 4 months ago

Hey @coratype,

Sorry for the late response. Here are my answers to your questions:

I’ve been working on a personal SwiftUI-based project for a bit and decided to try to incorporate this just to learn

This is a UIKit transition example that operates on UIViewControllers, UINavigationControllers, and UIKit delegates. You cannot integrate any of this directly into a SwiftUI app. While you might be able to use some concepts with the Introspect framework, I wouldn’t recommend going down that route.

Is there something specific I have to do? I set the project up as a SwiftUI project

There are no special setups used in this repo, it uses a default UIKit project and, as mentioned above, it won’t work with SwiftUI.

Regarding your issues with the mask and height constraints:

I don’t have time to debug your code, but I wrote a two-part article that discusses every single line of this project and provided a working example project. These should answer all your questions. If your code works in a clone of this repo, I suggest modifying it incrementally to test where you break it.

Regarding your comment on SwiftUI accomplishing this in 2 lines:

Yes, starting from iOS 18, you can achieve a similar zoom effect using matchedTransitionSource(id:in:). However:

I’m closing this issue. If you find a bug in the project, please feel free to open a new one.

Thanks!

coratype commented 4 months ago

thanks for responding but i personally feel like this is overtly complex and has far too much overhead to justify using over SwiftUI's approach, yes Swift's is a simple fade transition but it can much more easily be designed to be nearly as customizable as this approach and not nearly as buggy in my experience after trying both. when it comes to managing 2 lines of code vs the hundreds and several various files this entails, i think most will opt for latter

Kolos65 commented 4 months ago

You can opt for SwiftUI and target iOS 18, go with simpler transitions or just not go with a custom animation. This project was meant to give you a better understanding of how to craft a complex transition like this using UIKit if you are interested in custom view controller transitions. I guess you don't fall into this category.

coratype commented 4 months ago

my point is i dont think it has to be this complex and it doesnt really work well, as a way to learn how the mechanics of a transition like this work its fine but in a practical implementation the benefits of customization arent there. im curious what do you think swiftui's approach can't do that this can?

Kolos65 commented 4 months ago

What do you mean by "it doesnt really work well"? It works perfectly fine in the example project. What issue did you encounter regarding the transition?

Kolos65 commented 4 months ago

Correct me if i'm wrong but you can't implement the gesture driven interactive pop transition seen in this example with SwiftUI (even with iOS 18).

coratype commented 4 months ago

I was using rotated cards, stacked views, etc. and to use the Kit approach would have been ridiculous to manage personally. The slightest change would require rewriting several different files. 

Sometimes the kit approach would still flash white, it would not react well to scrolling quickly after popping, etc. With SwiftUI I have 3 lines of code and I’m good to go, it’s well worth it. Although you’re right about iOS18 support.

Re: Drag Gesture

https://x.com/ulrikstoch/status/1800483515202781689?s=46