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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Dialogs #1021

Merged
merged 8 commits into from
Jan 17, 2018
Merged

Added Dialogs #1021

merged 8 commits into from
Jan 17, 2018

Conversation

OrkhanAlikhanov
Copy link
Contributor

@OrkhanAlikhanov OrkhanAlikhanov commented Jan 12, 2018

The PR implements Material Design - Dialogs guideline and introduces 3 new classes:

  • DialogView - UIView subclass that implements dialog layout.
  • DialogController - UIViewController subclass that manages DialogView.
  • DialogBuilder - Delegate between developer and DialogController (Dialog API).

DialogView

Implements design and layout according to Material Design - Dialogs guideline

DialogController

DialogController is used to present dialog view and manage user's interactions with it.

Buttons

A standard dialog can contain three buttons at most. Those are positive, negative and neutral buttons. When user taps on one of those buttons, the dialog controller gets dismissed and corresponding event handler will be called.

vc.didTapPositiveButtonHandler = { print("User tapped on positive button") }
vc.didTapNegativeButtonHandler = { print("User tapped on negative button") }
vc.didTapNeutralButtonHandler = { print("User tapped on neutral button") }

Cancelable

DialogController can be dismissed when user taps outside of the dialog (on the dimmed area). To enable that feature, set isCancelable property to true. When user cancels the dialog (through tapping outside), didCancelHandler property of the dialog controller will be called.

vc.isCancelable = true
vc.didCancelHandler = { print("User tapped outside of the dialog") }

Preventing dismiss

DialogVontroller can be prevented from getting dismissed through user interaction. If shouldDismissHandler is set, then whenever dismissal happens shouldDismissHandler will be called and that's where you can prevent dismissal. The handler will accept two arguments and must return a Bool indicating approval of dismiss.

  • Dialog view (DialogView)
  • Button (Button?) that dismissal invoked through (nil if dialog is cancelled).
vc.shouldDismissHandler = { dialogView, button in
  // Decide wheter dialog should be dissmissed.
  return false // return true to approve dismissal, or false to prevent
}

Dialog API

Dialog Api simplifies creation of DialogController, allowing developer to focus on more important things.

Basic usage

Dialog()
    .title("Use Google's location service?")
    .details("Let Google help apps determine location...")
    .positive("AGREE") {
        print("User agreed")
    }
    .negative("DISAGREE") {
        print("User disagreed")
    }
    .neutral("CANCEL", handler: nil)
    .isCancelable(true) {
        print("Cancelled")
    }
    .shouldDismiss { dialogView, button in
    	// return button?.title == "AGREE"
        return true
    }
    .show(self) // self is the UIViewController that we're presenting dialog from
Result

The DialogController that is generated by Dialog API can be retrieved by simply writing .controller when desired properties are set (or even after dialog is shown).

let controller = Dialog()
                     .title("Use Google's location service?")
                     .details("Let Google help apps determine ...")
                     //...
                     .show(self)
                     .controller

Advanced

Dialog , DialogBuilder and DialogController is actually defined as

public typealias Dialog = DialogBuilder<DialogView>
open class DialogBuilder<T: DialogView> {}
open class DialogController<T: DialogView> {}

The generic way is just used to let developers work easily with the subclasses of DialogView.

Subclassing DialogView

Developers can subclass Material's DialogView to create their own custom dialogs.

class AppDialogView: DialogView {
    override func prepare() {
        super.prepare()
        titleLabel.textColor = Color.blue.base
        detailsLabel.textColor = Color.green.base
    }
 }

Then define AppDialog as

typealias AppDialog = DialogBuilder<AppDialogView>

And use it just like you would do Dialog

AppDialog()
    .title("My custom AppDialogView")
    .details("This is really coool!")
    .show(self)

More on customizing

There are 3 functions that is used by DialogView to determine its correct size and lay out its subviews:

  • func titleAreaSizeThatFits(width: CGFloat) -> CGSize
  • func contentViewSizeThatFits(width: CGFloat) -> CGSize
  • func buttonAreaSizeThatFits(width: CGFloat) -> CGSize
    If the overall size of the DialogView becomes more than the screen size of the device, the contentView will become scrollable.

If you customize titleArea, contentView or buttonArea, corresponding method should be overridden to return the size that is going to occupy and necessary laying out should be done in by overriding layoutSubviews()

Copy link
Contributor

@adamdahan adamdahan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this @DanielDahan but I would remove all the magic numbers and put them in private struct. Need to test this in real app.

@OrkhanAlikhanov
Copy link
Contributor Author

@adamdahan private struct is a good idea, I would apply that. But I've seen @DanielDahan himself using magic numbers and some magic variables throughout all the framework :]

@daniel-jonathan
Copy link
Member

@OrkhanAlikhanov @adamdahan I think that updating Material to no longer use any magic numbers would be ideal.

@OrkhanAlikhanov
Copy link
Contributor Author

Hey @DanielDahan, I actually started a while ago moving those magic numbers into a private struct under meaningful names as suggested by @adamdahan

@adamdahan
Copy link
Contributor

@OrkhanAlikhanov Thank you so much for this - I really like the code. I do have a question for you:

Did you consider using the Card as the Dialog itself?

Curious to know if there is a particular reason why you rolled out your own card like Dialog. Had you made an extension built on UIViewController or used a custom animated transition coordinator which allow cards to be presented modally - seems like the same task could been accomplished with less code and be more robust.

Looking forward to your response :)

Good work 👍

@OrkhanAlikhanov
Copy link
Contributor Author

@adamdahan Let's discuss this at gitter

@daniel-jonathan daniel-jonathan changed the base branch from master to development January 16, 2018 19:40
Copy link
Member

@daniel-jonathan daniel-jonathan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OrkhanAlikhanov Nicely done :) I looked through the code and tried the sample, all looks great. I am going to merge this and rework some parts to be better integrated with the framework. We will of course make sure to keep your credit as that is our protocol on our new site that is launching soon. Thank you!

@daniel-jonathan daniel-jonathan merged commit 5754015 into CosmicMind:development Jan 17, 2018
@OrkhanAlikhanov OrkhanAlikhanov mentioned this pull request Oct 16, 2018
@svachmic
Copy link

@OrkhanAlikhanov There has been some development on DialogBuilder -> Dialog and line:
typealias AppDialog = DialogBuilder<AppDialogView>
no longer works, not even
typealias AppDialog = Dialog<AppDialogView>.

What is the preferred way to subclass the dialog to have a custom feature in the dialog? Thanks!

@OrkhanAlikhanov
Copy link
Contributor Author

OrkhanAlikhanov commented Nov 19, 2018

Yeah, that was design decision. There is no more DialogBuilder we removed it in favor of separate Dialog object. You can use DialogBuilder code from this PR, or work directly with DialogController just like DialogBuilder does.

let vc = DialogController<AppDialogView>()
///configure vc.dialogView
present(vc, animated: true)

@svachmic
Copy link

Oh, so it's via the DialogController. No, that's perfectly fine, I just didn't know how to make it work, since it's a new undocumented feature in progress. Thanks a lot!

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

Successfully merging this pull request may close these issues.

4 participants