diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index ba050db50..70be2d629 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -903,6 +903,10 @@ EC85D757247E700F002C44C9 /* NSEMemoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC85D756247E700F002C44C9 /* NSEMemoryStore.swift */; }; EC87CFC925ADC10D00DB0A71 /* DialpadViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EC87CFC725ADC10D00DB0A71 /* DialpadViewController.storyboard */; }; EC87CFCA25ADC10D00DB0A71 /* DialpadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC87CFC825ADC10D00DB0A71 /* DialpadViewController.swift */; }; + EC87CFCD25ADC3D200DB0A71 /* DialpadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC87CFCC25ADC3D200DB0A71 /* DialpadButton.swift */; }; + EC87CFCF25ADC57600DB0A71 /* DialpadActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC87CFCE25ADC57600DB0A71 /* DialpadActionButton.swift */; }; + EC87CFD425AF427800DB0A71 /* CustomSizedPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC87CFD325AF427800DB0A71 /* CustomSizedPresentationController.swift */; }; + EC87CFD825AF6AD000DB0A71 /* CustomSizedPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC87CFD725AF6AD000DB0A71 /* CustomSizedPresentable.swift */; }; EC9A3EC524E1616900A8CFAE /* PushNotificationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9A3EC424E1616900A8CFAE /* PushNotificationStore.swift */; }; EC9A3EC624E1632C00A8CFAE /* PushNotificationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9A3EC424E1616900A8CFAE /* PushNotificationStore.swift */; }; EC9A3EC724E1634100A8CFAE /* KeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1CA87124C823E700DE9EBF /* KeyValueStore.swift */; }; @@ -2163,6 +2167,10 @@ EC85D756247E700F002C44C9 /* NSEMemoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEMemoryStore.swift; sourceTree = ""; }; EC87CFC725ADC10D00DB0A71 /* DialpadViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = DialpadViewController.storyboard; sourceTree = ""; }; EC87CFC825ADC10D00DB0A71 /* DialpadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DialpadViewController.swift; sourceTree = ""; }; + EC87CFCC25ADC3D200DB0A71 /* DialpadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialpadButton.swift; sourceTree = ""; }; + EC87CFCE25ADC57600DB0A71 /* DialpadActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialpadActionButton.swift; sourceTree = ""; }; + EC87CFD325AF427800DB0A71 /* CustomSizedPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSizedPresentationController.swift; sourceTree = ""; }; + EC87CFD725AF6AD000DB0A71 /* CustomSizedPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSizedPresentable.swift; sourceTree = ""; }; EC9A3EC424E1616900A8CFAE /* PushNotificationStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationStore.swift; sourceTree = ""; }; EC9E9B052575AF5C0007C0A0 /* CallVCPresentOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVCPresentOperation.swift; sourceTree = ""; }; EC9E9B072575AF730007C0A0 /* CallVCDismissOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVCDismissOperation.swift; sourceTree = ""; }; @@ -3772,6 +3780,7 @@ B1B556CD20EE6C4C00210D55 /* Common */ = { isa = PBXGroup; children = ( + EC87CFD225AF426000DB0A71 /* Presentation */, EC51E78E2510B7B900AAE7DB /* TableView */, ECF57A5F25093B04004BBF9D /* SectionHeaders */, ECF57A5C2509264B004BBF9D /* Views */, @@ -5238,10 +5247,29 @@ children = ( EC87CFC725ADC10D00DB0A71 /* DialpadViewController.storyboard */, EC87CFC825ADC10D00DB0A71 /* DialpadViewController.swift */, + EC87CFCB25ADC3C200DB0A71 /* Views */, ); path = Dialpad; sourceTree = ""; }; + EC87CFCB25ADC3C200DB0A71 /* Views */ = { + isa = PBXGroup; + children = ( + EC87CFCC25ADC3D200DB0A71 /* DialpadButton.swift */, + EC87CFCE25ADC57600DB0A71 /* DialpadActionButton.swift */, + ); + path = Views; + sourceTree = ""; + }; + EC87CFD225AF426000DB0A71 /* Presentation */ = { + isa = PBXGroup; + children = ( + EC87CFD325AF427800DB0A71 /* CustomSizedPresentationController.swift */, + EC87CFD725AF6AD000DB0A71 /* CustomSizedPresentable.swift */, + ); + path = Presentation; + sourceTree = ""; + }; EC9E9B042575AF460007C0A0 /* Operations */ = { isa = PBXGroup; children = ( @@ -6426,6 +6454,7 @@ 3232ABA3225730E100AD6A5C /* DeviceVerificationStartCoordinatorType.swift in Sources */, 3232AB4D2256558300AD6A5C /* TemplateScreenCoordinatorType.swift in Sources */, B1B5581720EF625800210D55 /* PreviewRoomTitleView.m in Sources */, + EC87CFD825AF6AD000DB0A71 /* CustomSizedPresentable.swift in Sources */, B1DCC63F22E9A3AE00625807 /* EmojiItem+EmojiMart.swift in Sources */, B1DCC61C22E5E17100625807 /* EmojiPickerViewAction.swift in Sources */, B1098BDF21ECE09F000DDA48 /* Strings.swift in Sources */, @@ -6556,6 +6585,7 @@ 3232AB4E2256558300AD6A5C /* TemplateScreenViewModelType.swift in Sources */, 32DB557D22FDADE50016329E /* ServiceTermsModalScreenCoordinatorType.swift in Sources */, B1B5590520EF768F00210D55 /* RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m in Sources */, + EC87CFD425AF427800DB0A71 /* CustomSizedPresentationController.swift in Sources */, 3232ABA5225730E100AD6A5C /* DeviceVerificationStartViewModelType.swift in Sources */, B1B336C1242B933700F95EC4 /* KeyVerificationSelfVerifyStartCoordinatorType.swift in Sources */, EC85D71B2477DCD7002C44C9 /* KeyVerificationScanConfirmationCoordinatorType.swift in Sources */, @@ -6661,6 +6691,7 @@ B12676892523E4D100BE6B98 /* SecretsResetViewAction.swift in Sources */, B1B5573820EE6C4D00210D55 /* GroupParticipantsViewController.m in Sources */, EC51E7B02514D2E100AAE7DB /* RoomInfoCoordinatorType.swift in Sources */, + EC87CFCF25ADC57600DB0A71 /* DialpadActionButton.swift in Sources */, 3232ABAB225730E100AD6A5C /* KeyVerificationCoordinator.swift in Sources */, B1BEE73B23DF44A60003A4CB /* UserVerificationSessionsStatusCoordinator.swift in Sources */, B1B5583E20EF6E7F00210D55 /* GroupRoomTableViewCell.m in Sources */, @@ -6670,6 +6701,7 @@ EC85D73A2477DDB8002C44C9 /* DirectorySectionHeaderContainerView.m in Sources */, ECFBD5E3250B6CAB00DD5F5A /* ChooseAvatarTableViewCellVM.swift in Sources */, EC711B8524A63B37008F830C /* SecretsRecoveryWithPassphraseCoordinator.swift in Sources */, + EC87CFCD25ADC3D200DB0A71 /* DialpadButton.swift in Sources */, 32863A5C2384074C00D07C4A /* RiotSettingAllowedWidgets.swift in Sources */, EC711B8724A63B37008F830C /* SecretsRecoveryWithPassphraseViewModelType.swift in Sources */, B1B9DEDA22E9B7350065E677 /* SerializationService.swift in Sources */, diff --git a/Riot/Modules/Call/Dialpad/DialpadViewController.storyboard b/Riot/Modules/Call/Dialpad/DialpadViewController.storyboard index f96873bc6..b8393781e 100644 --- a/Riot/Modules/Call/Dialpad/DialpadViewController.storyboard +++ b/Riot/Modules/Call/Dialpad/DialpadViewController.storyboard @@ -1,14 +1,16 @@ - + - + + + - + @@ -16,127 +18,280 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + - - - - - - + + + + + + + @@ -145,6 +300,11 @@ - + + + + + + diff --git a/Riot/Modules/Call/Dialpad/DialpadViewController.swift b/Riot/Modules/Call/Dialpad/DialpadViewController.swift index a53358c76..176818750 100644 --- a/Riot/Modules/Call/Dialpad/DialpadViewController.swift +++ b/Riot/Modules/Call/Dialpad/DialpadViewController.swift @@ -17,27 +17,64 @@ */ import UIKit +import libPhoneNumber_iOS -protocol DialpadViewControllerDelegate: class { - func dialpadViewControllerDidTapSetupAction(_ viewController: DialpadViewController) - func dialpadViewControllerDidCancel(_ viewController: DialpadViewController) +@objc protocol DialpadViewControllerDelegate: class { + func dialpadViewControllerDidTapCall(_ viewController: DialpadViewController, withPhoneNumber phoneNumber: String) + func dialpadViewControllerDidTapClose(_ viewController: DialpadViewController) } -final class DialpadViewController: UIViewController { - - // MARK: - Properties +@objcMembers +class DialpadViewController: UIViewController { // MARK: Outlets - @IBOutlet private weak var logoImageView: UIImageView! + @IBOutlet private weak var closeButton: UIButton! @IBOutlet private weak var titleLabel: UILabel! - @IBOutlet private weak var informationLabel: UILabel! - - @IBOutlet private weak var okButtonBackgroundView: UIView! - @IBOutlet private weak var okButton: UIButton! + @IBOutlet private weak var phoneNumberTextField: UITextField! { + didSet { + phoneNumberTextField.text = nil + // avoid showing keyboard on text field + phoneNumberTextField.inputView = UIView() + phoneNumberTextField.inputAccessoryView = UIView() + } + } + @IBOutlet private weak var lineView: UIView! + @IBOutlet private weak var digitsStackView: UIStackView! + @IBOutlet private weak var backspaceButton: DialpadActionButton! { + didSet { + backspaceButton.type = .backspace + } + } + @IBOutlet private weak var callButton: DialpadActionButton! { + didSet { + callButton.type = .call + } + } // MARK: Private + private enum Constants { + static let sizeOniPad: CGSize = CGSize(width: 375, height: 667) + } + + private var wasCursorAtTheEnd: Bool = true + + /// Phone number as formatted + private var phoneNumber: String = "" { + willSet { + wasCursorAtTheEnd = isCursorAtTheEnd() + } didSet { + phoneNumberTextField.text = phoneNumber + if wasCursorAtTheEnd { + moveCursorToTheEnd() + } + } + } + /// Phone number as non-formatted + private var rawPhoneNumber: String { + return phoneNumber.vc_removingAllWhitespaces() + } private var theme: Theme! // MARK: Public @@ -59,10 +96,7 @@ final class DialpadViewController: UIViewController { // Do any additional setup after loading the view. - self.title = "Template" - self.vc_removeBackTitle() - - self.setupViews() + titleLabel.text = VectorL10n.dialpadTitle self.registerThemeServiceDidChangeThemeNotification() self.update(theme: self.theme) } @@ -73,38 +107,73 @@ final class DialpadViewController: UIViewController { // MARK: - Private - private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - self?.cancelButtonAction() + private func isCursorAtTheEnd() -> Bool { + guard let selectedRange = phoneNumberTextField.selectedTextRange else { + return true } - self.navigationItem.rightBarButtonItem = cancelBarButtonItem -// let logoImage = Asset.Images.* -// self.logoImageView.image = keybackupLogoImage - -// self.titleLabel.text = VectorL10n.xxxxTitle -// self.informationLabel.text = VectorL10n.xxxxDescription -// -// self.okButton.setTitle(VectorL10n.xxxxAction, for: .normal) + if !selectedRange.isEmpty { + return false + } + + let cursorEndPos = phoneNumberTextField.offset(from: phoneNumberTextField.beginningOfDocument, to: selectedRange.end) + + return cursorEndPos == phoneNumber.count + } + + private func moveCursorToTheEnd() { + guard let cursorPos = phoneNumberTextField.position(from: phoneNumberTextField.beginningOfDocument, + offset: phoneNumber.count) else { return } + + phoneNumberTextField.selectedTextRange = phoneNumberTextField.textRange(from: cursorPos, + to: cursorPos) + } + + private func reformatPhoneNumber() { + guard let phoneNumberUtil = NBPhoneNumberUtil.sharedInstance() else { + // no formatter + return + } + + do { + // try formatting the number + if phoneNumber.hasPrefix("00") { + let range = phoneNumber.startIndex.. CGSize { + return Constants.sizeOniPad + } + } diff --git a/Riot/Modules/Call/Dialpad/Views/DialpadActionButton.swift b/Riot/Modules/Call/Dialpad/Views/DialpadActionButton.swift new file mode 100644 index 000000000..3426269d5 --- /dev/null +++ b/Riot/Modules/Call/Dialpad/Views/DialpadActionButton.swift @@ -0,0 +1,39 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// Dialpad action button type +@objc enum DialpadActionButtonType: Int { + case backspace + case call +} + +/// Action button class for Dialpad screen +class DialpadActionButton: DialpadButton { + + var type: DialpadActionButtonType = .backspace + + override func update(theme: Theme) { + switch type { + case .backspace: + backgroundColor = theme.warningColor + case .call: + backgroundColor = theme.tintColor + } + } + +} diff --git a/Riot/Modules/Call/Dialpad/Views/DialpadButton.swift b/Riot/Modules/Call/Dialpad/Views/DialpadButton.swift new file mode 100644 index 000000000..c8e55b5ea --- /dev/null +++ b/Riot/Modules/Call/Dialpad/Views/DialpadButton.swift @@ -0,0 +1,56 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// Digit button class for Dialpad screen +class DialpadButton: UIButton { + + private enum Constants { + static let size: CGSize = CGSize(width: 68, height: 68) + } + + init() { + super.init(frame: CGRect(origin: .zero, size: Constants.size)) + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + clipsToBounds = true + layer.cornerRadius = Constants.size.width/2 + } + +} + +// MARK: - Themable + +extension DialpadButton: Themable { + + func update(theme: Theme) { + setTitleColor(theme.textPrimaryColor, for: .normal) + backgroundColor = theme.headerBackgroundColor + } + +} diff --git a/Riot/Modules/Common/Presentation/CustomSizedPresentable.swift b/Riot/Modules/Common/Presentation/CustomSizedPresentable.swift new file mode 100644 index 000000000..50c2045cf --- /dev/null +++ b/Riot/Modules/Common/Presentation/CustomSizedPresentable.swift @@ -0,0 +1,30 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Protocol to be used with a `CustomSizedPresentationController` +@objc protocol CustomSizedPresentable { + + /// Custom size for presentable. If not implemented, the presentable will have the both half width and height of the container view. + /// - Parameter containerSize: Container view's size. + @objc optional func customSize(withParentContainerSize containerSize: CGSize) -> CGSize + + /// Position (origin) of presentable in container. If not implemented, the presentable will be centered to the container view. + /// - Parameter containerSize: Container view's size. + @objc optional func position(withParentContainerSize containerSize: CGSize) -> CGPoint + +} diff --git a/Riot/Modules/Common/Presentation/CustomSizedPresentationController.swift b/Riot/Modules/Common/Presentation/CustomSizedPresentationController.swift new file mode 100644 index 000000000..6d5e4557b --- /dev/null +++ b/Riot/Modules/Common/Presentation/CustomSizedPresentationController.swift @@ -0,0 +1,233 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +/// Controller for custom sized presentations. By default, presented view controller will be sized as both half of the screen in width and height, and will be centered to the screen. Implement `CustomSizedPresentable` in presented view controller to change that if needed. This class can also be set as `transitioningDelegate` as presented view controller, as it's conforming `UIViewControllerTransitioningDelegate`. +@objcMembers +class CustomSizedPresentationController: UIPresentationController { + + // MARK: - Public Properties + + /// Corner radius for presented view controller's view. Default value is `8.0`. + var cornerRadius: CGFloat = 8.0 + + /// Background color of dimming view, which is located behind the presented view controller's view. Default value is `white with 0.5 alpha`. + var dimColor: UIColor = UIColor(white: 0.0, alpha: 0.5) + + /// Dismiss view controller when background tapped. Default value is `true`. + var dismissOnBackgroundTap: Bool = true + + // MARK: - Private Properties + + /// Dim view + private var dimmingView: UIView! + + /// Wrapper view for presentation. It's introduced to handle corner radius on presented view controller's view and it's superview of all other views. + private var presentationWrappingView: UIView! + + // MARK: - Initializer + + override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { + super.init(presentedViewController: presentedViewController, presenting: presentingViewController) + + presentedViewController.modalPresentationStyle = .custom + } + + // MARK: - Actions + + @objc private func dimmingViewTapped(_ sender: UITapGestureRecognizer) { + if dismissOnBackgroundTap { + presentedViewController.dismiss(animated: true, completion: nil) + } + } + + // MARK: - Presentation + + override func presentationTransitionWillBegin() { + guard let presentedViewControllerView = super.presentedView else { return } + + // Wrap the presented view controller's view in an intermediate hierarchy + // that applies a shadow and rounded corners to the top-left and top-right + // edges. The final effect is built using three intermediate views. + // + // presentationWrapperView <- shadow + // |- presentationRoundedCornerView <- rounded corners (masksToBounds) + // |- presentedViewControllerWrapperView + // |- presentedViewControllerView (presentedViewController.view) + // + // SEE ALSO: The note in AAPLCustomPresentationSecondViewController.m. + do { + let presentationWrapperView = UIView(frame: frameOfPresentedViewInContainerView) + presentationWrapperView.layer.shadowOffset = CGSize(width: 0, height: -2) + presentationWrapperView.layer.shadowRadius = 10 + presentationWrapperView.layer.shadowColor = UIColor(white: 0, alpha: 0.5).cgColor + presentationWrappingView = presentationWrapperView + + // presentationRoundedCornerView is CORNER_RADIUS points taller than the + // height of the presented view controller's view. This is because + // the cornerRadius is applied to all corners of the view. Since the + // effect calls for only the top two corners to be rounded we size + // the view such that the bottom CORNER_RADIUS points lie below + // the bottom edge of the screen. + let cornerViewRect = presentationWrapperView.bounds//.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: -cornerRadius, right: 0)) + + let presentationRoundedCornerView = UIView(frame: cornerViewRect) + presentationRoundedCornerView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + presentationRoundedCornerView.layer.cornerRadius = cornerRadius + presentationRoundedCornerView.layer.masksToBounds = true + + // To undo the extra height added to presentationRoundedCornerView, + // presentedViewControllerWrapperView is inset by CORNER_RADIUS points. + // This also matches the size of presentedViewControllerWrapperView's + // bounds to the size of -frameOfPresentedViewInContainerView. + let wrapperRect = presentationRoundedCornerView.bounds + + let presentedViewControllerWrapperView = UIView(frame: wrapperRect) + presentedViewControllerWrapperView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + // Add presentedViewControllerView -> presentedViewControllerWrapperView. + presentedViewControllerView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + presentedViewControllerView.frame = presentedViewControllerWrapperView.bounds + presentedViewControllerWrapperView.addSubview(presentedViewControllerView) + + // Add presentedViewControllerWrapperView -> presentationRoundedCornerView. + presentationRoundedCornerView.addSubview(presentedViewControllerWrapperView) + + // Add presentationRoundedCornerView -> presentationWrapperView. + presentationWrapperView.addSubview(presentationRoundedCornerView) + } + + // Add a dimming view behind presentationWrapperView. self.presentedView + // is added later (by the animator) so any views added here will be + // appear behind the -presentedView. + do { + let dimmingView = UIView(frame: containerView?.bounds ?? .zero) + dimmingView.backgroundColor = dimColor + dimmingView.isOpaque = false + dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapped(_:))) + dimmingView.addGestureRecognizer(tapGestureRecognizer) + + self.dimmingView = dimmingView + containerView?.addSubview(dimmingView) + + // Get the transition coordinator for the presentation so we can + // fade in the dimmingView alongside the presentation animation. + let transitionCoordinator = self.presentingViewController.transitionCoordinator + + dimmingView.alpha = 0.0 + transitionCoordinator?.animate(alongsideTransition: { _ in + self.dimmingView?.alpha = 1.0 + }, completion: nil) + } + } + + override func presentationTransitionDidEnd(_ completed: Bool) { + if !completed { + presentationWrappingView = nil + dimmingView = nil + } + } + + // MARK: - Dismissal + + override func dismissalTransitionWillBegin() { + guard let coordinator = presentingViewController.transitionCoordinator else { + dimmingView.alpha = 0.0 + return + } + + coordinator.animate(alongsideTransition: { _ in + self.dimmingView.alpha = 0.0 + }) + } + + override func dismissalTransitionDidEnd(_ completed: Bool) { + if completed { + presentationWrappingView = nil + dimmingView = nil + } + } + + // MARK: - Overrides + + override var presentedView: UIView? { + return presentationWrappingView + } + + override func size(forChildContentContainer container: UIContentContainer, + withParentContainerSize parentSize: CGSize) -> CGSize { + guard container === presentedViewController else { + return super.size(forChildContentContainer: container, withParentContainerSize: parentSize) + } + + // return value from presentable if implemented + if let presentable = presentedViewController as? CustomSizedPresentable, let customSize = presentable.customSize?(withParentContainerSize: parentSize) { + return customSize + } + + // half of the width/height by default + return CGSize(width: parentSize.width/2.0, height: parentSize.height/2.0) + } + + override var frameOfPresentedViewInContainerView: CGRect { + guard let containerView = containerView else { + return super.frameOfPresentedViewInContainerView + } + + let size = self.size(forChildContentContainer: presentedViewController, + withParentContainerSize: containerView.bounds.size) + + // use origin value from presentable if implemented + if let presentable = presentedViewController as? CustomSizedPresentable, let origin = presentable.position?(withParentContainerSize: containerView.bounds.size) { + return CGRect(origin: origin, size: size) + } + + // center presented view by default + let origin = CGPoint(x: (containerView.bounds.width - size.width)/2, + y: (containerView.bounds.height - size.height)/2) + + return CGRect(origin: origin, size: size) + } + + override func containerViewWillLayoutSubviews() { + super.containerViewWillLayoutSubviews() + + self.dimmingView?.frame = containerView?.bounds ?? .zero + self.presentationWrappingView?.frame = frameOfPresentedViewInContainerView + } + + override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { + super.preferredContentSizeDidChange(forChildContentContainer: container) + + if container === presentedViewController { + self.containerView?.setNeedsLayout() + } + } + +} + +// MARK: - UIViewControllerTransitioningDelegate + +extension CustomSizedPresentationController: UIViewControllerTransitioningDelegate { + + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + return CustomSizedPresentationController(presentedViewController: presented, presenting: presenting) + } + +}