Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support using the library from UIKit #4

Merged
merged 1 commit into from Oct 4, 2022

Conversation

kevinrpb
Copy link
Contributor

@kevinrpb kevinrpb commented Oct 2, 2022

Hi! I love the library, thank you for making it!

This PR is more of a suggestion/discussion prompt to see what would be the best approach to support using WelcomeSheet from UIKit.

I was looking into integrating some SwiftUI on a UIKit app, and seems like the onboarding might be a good place for it using WelcomeSheet. I wasn't sure of how I should tackle it, since this was designed to be used as a view modifier, so I came up with the attached helper class.

It takes advantage of your ModalUIHostingController to create an erased UIViewController that can be used from UIKit to be presented. Here is how it would be used:

final class ViewController: UIViewController {
    // ... All your setup

    // MARK: Welcome Sheet
    private var welcomeSheetController: UIViewController?

    func showWelcome() {
        let pages: [WelcomeSheetPage] = [
            .init(
                title: "Welcome to my App!",
                rows: [
                    .init(
                        imageSystemName: "questionmark.circle",
                        title: "Lorem Ipsum",
                        content: "dolor sit amet"
                    ),
                ],
                mainButtonTitle: "OK!"
            )
        ]

        self.welcomeSheetController = WelcomeSheetController.get(pages: pages) { [weak self] in
            self?.welcomeSheetController?.dismiss(animated: true)
        }

        present(self.welcomeSheetController!, animated: true)
    }
}

Of course this is a simple and naive approach to it (my goal was to not change any APIs for now). Ideally, WelcomeSheetView would be exposed to use in an arbitrary UIHostingController but I'm not sure of what would need to be done to adapt the logic to decouple the WelcomeSheetView from the ModalUIHostingController as it is now, since it uses the environment to handle the dismissal.

An idea would be to make WelcomeSheetView receive a closure that handles the tap in the last page's button. This way, the FormSheet would handle the dismissal for SwiftUI and in UIKit world the caller would be responsible to do it in a similar way to my example.

Let me know if all this makes sense or if I am missing something and there is a better approach!

Cheers 馃槃

@MAJKFL MAJKFL merged commit f6f3ef4 into MAJKFL:main Oct 4, 2022
@MAJKFL
Copy link
Owner

MAJKFL commented Oct 4, 2022

Hey, thanks for your PR! Bringing Welcome sheet to UIKit is a wonderful idea! Passing closure responsible for closing is imho the best way. I played around with it and even though it required a lot of work under the hood especially in ModalUIViewController it works fine now. Importantly my changes don鈥檛 change anything in the modifiers itself so there鈥檚 nothing to worry about in terms of backwards compatibility. Please take a look on the last commit and I would love to hear your feedback.

public class WelcomeSheetController {
    var pages: [WelcomeSheetPage]
    var isSlideToDismissDisabled: Bool
    var onDismiss: () -> Void
    
    init(pages: [WelcomeSheetPage], isSlideToDismissDisabled: Bool = false, onDismiss: @escaping () -> Void = {}) {
        self.pages = pages
        self.isSlideToDismissDisabled = isSlideToDismissDisabled
        self.onDismiss = onDismiss
    }

    func get() -> UIViewController {
        let hc = ModalUIHostingController(rootView: WelcomeSheetView(pages: pages, onDismiss: onDismiss))
        hc.isModalInPresentation = isSlideToDismissDisabled
        return hc
    }
    
    public static func get(pages: [WelcomeSheetPage], isSlideToDismissDisabled: Bool = false, onDismiss: @escaping () -> Void = {}) -> UIViewController {
        WelcomeSheetController(pages: pages, isSlideToDismissDisabled: isSlideToDismissDisabled, onDismiss: onDismiss).get()
    }
}

I haven鈥檛 switched from ModalUIHostingController to arbitrary UIHostingController though. This approach allowed me to easily modify the closure inside WelcomeSheetView.

class ModalUIHostingController: UIHostingController<WelcomeSheetView>, UIPopoverPresentationControllerDelegate {
    var onDismiss: () -> Void
    
    required init?(coder: NSCoder) { fatalError("") }
    
    override init(rootView: WelcomeSheetView) {
        self.onDismiss = rootView.onDismiss
        super.init(rootView: rootView)
        
        self.rootView.onDismiss = { [weak self] in
            rootView.onDismiss()
            self?.dismiss(animated: true)
        }
        
        modalPresentationStyle = .formSheet
        preferredContentSize = CGSize(width: iPadSheetDimensions.width, height: iPadSheetDimensions.height)
        presentationController?.delegate = self
    }
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { onDismiss() }
}

Also took this opportunity to clean up the code a little. Apparently a couple of months back I was a really bad programmer 馃檭.

Looking forward to hearing from you 馃榾

@kevinrpb
Copy link
Contributor Author

kevinrpb commented Oct 6, 2022

Hi! Glad the PR was welcomed, happy to help!

With the changes you made to ModalUIHostingController, it should now be even easier to create a UIKit interface via a controller. I made two commits to the uikit-support branch in my fork.

First, the WelcomeSheetController can now be a subclass of ModalUIHostingController (see below). I think this fits better within the UIKit environment than the previous method. The only 'catch' is that ModalUIHostingController and WelcomeSheetView have to be declared public.

Second, I added another demo project to showcase the UIkit version. Here, I also set the dependency of both demo project to point to the local package (the parent directory). This way, the demo uses the current changes to build and it's easier to test further changes (IMHO, anyways!).

Finally, I added a new init to WelcomeSheetPageRow. This one takes a imageNamed: String parameter to load images from the bundle by name. This way there's no need to call the Image.init from UIKit. For Color this is less of an issue since we can use the implicit member (e.g. .cyan) and it won't look out of place, but perhaps we can also add UIColor versions of the init in the future.

Let me know what you think about the changes and I'll open a PR with them. Also tell me if I should modify anything 馃槃

/// WelcomeSheetController declaration

public class WelcomeSheetController: ModalUIHostingController {
    public init(pages: [WelcomeSheetPage], isSlideToDismissDisabled: Bool = false, onDismiss: @escaping () -> Void = {}) {
        super.init(rootView: WelcomeSheetView(pages: pages, onDismiss: onDismiss))

        self.isModalInPresentation = isSlideToDismissDisabled
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

/// Then in our app..

class ViewController: UIViewController {
    @IBOutlet weak var showSheetButton: UIButton!

    let pages: [WelcomeSheetPageRow] = [/*....*/]

    override func viewDidLoad() {
        super.viewDidLoad()

        showSheetButton.addTarget(self, action: #selector(showSheet), for: .touchUpInside)
    }

    @objc func showSheet() {
        let sheetVC = WelcomeSheetController(pages: self.pages, isSlideToDismissDisabled: true, onDismiss: sheetDismissed)

        present(sheetVC, animated: true)
    }

    func sheetDismissed() {
        print("Sheet dismissed")
    }

@MAJKFL
Copy link
Owner

MAJKFL commented Oct 7, 2022

Hey, everything seems awesome, I love your implementation of WelcomeSheetController and thank you for adding the demo project too. That鈥檚 a great idea with WelcomeSheetPageRow new init, great it will remain backwards compatible and I agree the analogical function for UIColor might be great to remain consistent. Also could you update readme with all the recent changes? Then we can merge new PR and make a release 馃檪

@kevinrpb
Copy link
Contributor Author

kevinrpb commented Oct 8, 2022

Sure, I'll get down to it when I have some spare time 馃槃

@MAJKFL
Copy link
Owner

MAJKFL commented Nov 14, 2022

Hey, have you got time to look at it? If you haven鈥檛 had an opportunity I can update readme by myself, please just create a PR with the updated repo. I would like to create a new release soon, there is one feature on a different branch waiting to be included in the release too

@kevinrpb
Copy link
Contributor Author

Hiya! Life err... got in the way 馃槄. I will try to do it during the weekend :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants