feat: added bodal wrapper view controller
This commit is contained in:
parent
86fddafcdf
commit
808d40eca5
|
|
@ -1,8 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
### 1.46.0
|
||||
### 1.50.0
|
||||
|
||||
- **Added**: `BaseModalViewController` implementing `PanModalPresentable` with additional functionality
|
||||
- **Added**: `BaseModalWrapperViewController` for wrapping `UIViewController`s with `BaseModalViewController` functionality
|
||||
- **Updated**: Helper methods for `WrappedLayout` and `UIEdgeInsets`
|
||||
|
||||
### 1.45.0
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ let package = Package(
|
|||
.target(name: "TIBottomSheet",
|
||||
dependencies: ["TIUIElements", "TIUIKitCore", "TISwiftUtils"],
|
||||
path: "TIBottomSheet/Sources",
|
||||
exclude: ["TIBottomSheet.app"],
|
||||
exclude: ["../TIBottomSheet.app"],
|
||||
plugins: [.plugin(name: "TISwiftLintPlugin")]),
|
||||
|
||||
// MARK: - SwiftUI
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// Copyright (c) 2023 Touch Instinct
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the Software), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
open class BaseModalWrapperViewController<ContentViewController>: BaseModalViewController<UIView, UIView>
|
||||
where ContentViewController: UIViewController {
|
||||
|
||||
private(set) public lazy var contentViewController = createContentViewController()
|
||||
|
||||
|
||||
// MARK: - BaseModalViewController
|
||||
|
||||
open override func createContentView() -> UIView {
|
||||
contentViewController.view
|
||||
}
|
||||
|
||||
// MARK: - Open methods
|
||||
|
||||
open func createContentViewController() -> ContentViewController {
|
||||
ContentViewController()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# gitignore nef files
|
||||
**/build/
|
||||
**/nef/
|
||||
LICENSE
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
TIBottomSheet содержить базовую реализацию модального котроллера и немного видоизмененную библиотеку PanModal.
|
||||
|
||||
# Базовый контроллер
|
||||
## Базовый контроллер
|
||||
|
||||
Для создания модального котроллера можно унаследоваться от `BaseModalViewController`. Данный клас принимает два generic типа: тип основного контента, тип контента футера.
|
||||
*/
|
||||
|
|
@ -12,6 +12,28 @@ import UIKit
|
|||
|
||||
class EmptyViewController: BaseModalViewController<UIView, UIView> { }
|
||||
|
||||
/*:
|
||||
## Обертка вокруг существующего контроллера
|
||||
|
||||
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
|
||||
*/
|
||||
|
||||
import TIUIKitCore
|
||||
|
||||
final class OldMassiveViewController: BaseInitializableViewController {
|
||||
// some implementation
|
||||
}
|
||||
|
||||
typealias ModalOldMassiveViewController = BaseModalWrapperViewController<OldMassiveViewController>
|
||||
|
||||
class PresentingViewController: BaseInitializableViewController {
|
||||
// some implementation
|
||||
|
||||
@objc private func onButtonTapped() {
|
||||
presentPanModal(ModalOldMassiveViewController())
|
||||
}
|
||||
}
|
||||
|
||||
/*:
|
||||
## Контент модального контроллера
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
public extension Nef {
|
||||
|
||||
static func run<T: XCTestCase>(testCase class: T.Type) {
|
||||
startTestObserver()
|
||||
T.defaultTestSuite.run()
|
||||
}
|
||||
|
||||
static private func startTestObserver() {
|
||||
_ = testObserverInstalled
|
||||
}
|
||||
|
||||
static private var testObserverInstalled = { () -> NefTestFailObserver in
|
||||
let testObserver = NefTestFailObserver()
|
||||
XCTestObservationCenter.shared.addTestObserver(testObserver)
|
||||
return testObserver
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: enrich the output for XCTest
|
||||
fileprivate class NefTestFailObserver: NSObject, XCTestObservation {
|
||||
|
||||
private var numberOfFailedTests = 0
|
||||
|
||||
func testSuiteWillStart(_ testSuite: XCTestSuite) {
|
||||
numberOfFailedTests = 0
|
||||
}
|
||||
|
||||
func testSuiteDidFinish(_ testSuite: XCTestSuite) {
|
||||
if numberOfFailedTests > 0 {
|
||||
print("💢 Test Suite '\(testSuite.name)' finished with \(numberOfFailedTests) failed \(numberOfFailedTests > 1 ? "tests" : "test").")
|
||||
} else {
|
||||
print("🔅 Test Suite '\(testSuite.name)' finished successfully.")
|
||||
}
|
||||
}
|
||||
|
||||
func testCase(_ testCase: XCTestCase,
|
||||
didFailWithDescription description: String,
|
||||
inFile filePath: String?,
|
||||
atLine lineNumber: Int) {
|
||||
|
||||
numberOfFailedTests += 1
|
||||
print("❗️Test Fail '\(testCase.name)':\(UInt(lineNumber)): \(description.description)")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true' executeOnSourceChanges='true'/>
|
||||
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true'/>
|
||||
|
|
@ -6,9 +6,16 @@
|
|||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.debug.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
8BACBE8322576CAD00266845 /* TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.release.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -16,17 +23,38 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
11F06D2789C6CF40767861CF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1F7782E3A7AD7291B7C09F56 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */,
|
||||
AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8B39A26221D40F8700DE2643 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8BACBE8422576CAD00266845 /* TIBottomSheet */,
|
||||
8B39A26C21D40F8700DE2643 /* Products */,
|
||||
1F7782E3A7AD7291B7C09F56 /* Pods */,
|
||||
11F06D2789C6CF40767861CF /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -63,6 +91,7 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */;
|
||||
buildPhases = (
|
||||
4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */,
|
||||
8BACBE7E22576CAD00266845 /* Headers */,
|
||||
8BACBE7F22576CAD00266845 /* Sources */,
|
||||
8BACBE8022576CAD00266845 /* Frameworks */,
|
||||
|
|
@ -120,6 +149,31 @@
|
|||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-TIBottomSheet-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
8BACBE7F22576CAD00266845 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
|
|
@ -254,6 +308,7 @@
|
|||
};
|
||||
8BACBE8822576CAD00266845 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
|
|
@ -284,6 +339,7 @@
|
|||
};
|
||||
8BACBE8922576CAD00266845 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
|
|
|
|||
11
TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcworkspace/contents.xcworkspacedata
generated
Normal file
11
TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef location = "group:TIBottomSheet.playground"></FileRef>
|
||||
<FileRef
|
||||
location = "group:TIBottomSheet.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class BaseViewSkeletonsConfiguration {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
//
|
||||
|
||||
import TISwiftUtils
|
||||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
open class TextSkeletonsConfiguration: BaseViewSkeletonsConfiguration {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import TIUIElements
|
||||
import UIKit
|
||||
|
||||
public final class ScrollViewWrapper<ContentView: UIView>: UIScrollView {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Pod::Spec.new do |s|
|
|||
s.ios.deployment_target = '11.0'
|
||||
s.swift_versions = ['5.7']
|
||||
|
||||
sources = '/Sources/**/*'
|
||||
sources = 'Sources/**/*'
|
||||
if ENV["DEVELOPMENT_INSTALL"] # installing using :path =>
|
||||
s.source_files = sources
|
||||
s.exclude_files = s.name + '.app'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
|
||||
# TIBottomSheet
|
||||
|
||||
TIBottomSheet содержить базовую реализацию модального котроллера и немного видоизмененную библиотеку PanModal.
|
||||
|
||||
## Базовый контроллер
|
||||
|
||||
Для создания модального котроллера можно унаследоваться от `BaseModalViewController`. Данный клас принимает два generic типа: тип основного контента, тип контента футера.
|
||||
|
||||
```swift
|
||||
import TIBottomSheet
|
||||
import UIKit
|
||||
|
||||
class EmptyViewController: BaseModalViewController<UIView, UIView> { }
|
||||
```
|
||||
|
||||
## Обертка вокруг существующего контроллера
|
||||
|
||||
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
|
||||
|
||||
```swift
|
||||
import TIUIKitCore
|
||||
|
||||
final class OldMassiveViewController: BaseInitializableViewController {
|
||||
// some implementation
|
||||
}
|
||||
|
||||
typealias ModalOldMassiveViewController = BaseModalWrapperViewController<OldMassiveViewController>
|
||||
|
||||
class PresentingViewController: BaseInitializableViewController {
|
||||
// some implementation
|
||||
|
||||
@objc private func onButtonTapped() {
|
||||
presentPanModal(ModalOldMassiveViewController())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Контент модального контроллера
|
||||
|
||||
Модальный котроллер может содержать следующие элементы: `DragView`, `HeaderView`, `FooterView`. Каждый из них является опциональным и без дополнительных настроек не будет показываться.
|
||||
|
||||
DragView - небольшая view, за которую пользователь "держит" модальный контроллер
|
||||
HeaderView - контейнер, содержащий в себе кнопки назад/закрыть или какие-то другие элементы управления
|
||||
FooterView - view, располагающаяся внизу контроллера, поверх всего контента (модальный контроллер уже настроен так, чтобы при скролле в самый низ, футер не перекрывал последнюю ячейку)
|
||||
|
||||
Для настройки каждого у котроллера есть свойство `viewControllerAppearance`. Через него будет настраиваться весь контроллер. Однако стоит заметить, что котроллер не будет настраивать передаваимую вью, содержащую основной контент. Стандартно котроллер будет пытаться расположить контент так, чтобы он заполнил все пространство.
|
||||
|
||||
Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой:
|
||||
|
||||
```swift
|
||||
class CustomViewController: BaseModalViewController<UIView, UIView> {
|
||||
override var viewControllerAppearance: BaseAppearance {
|
||||
let appearance = super.viewControllerAppearance
|
||||
|
||||
appearance.dragViewState = .presented(.defaultAppearance)
|
||||
appearance.headerViewState = .presented(.make {
|
||||
$0.backgroundColor = .white
|
||||
$0.contentViewState = .buttonLeft(.init(titles: [.normal: "Close"],
|
||||
appearance: [.normal: .init(backgroundColor: .white)]))
|
||||
})
|
||||
|
||||
return appearance
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## "Якори" контроллера
|
||||
|
||||
Раньше для настройки высоты контроллера необходимо было пользоваться свойствами `longFormHeight`, `shortFormHeight`. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться:
|
||||
|
||||
```swift
|
||||
class DetentsViewController: BaseModalViewController<UIView, UIView> {
|
||||
override var presentationDetents: [ModalViewPresentationDetent] {
|
||||
[.headerOnly, .height(300), .maxHeight]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их
|
||||
- height(_) будет показывать контроллер на переданной высоте
|
||||
- maxHeight - вся высота экрана (до safeArea)
|
||||
|
||||
В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране.
|
||||
|
||||
## DimmedView
|
||||
|
||||
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedViewType`. Это перечисление, содержащие следующие кейсы:
|
||||
|
||||
- opaque: dimmedView не прозрачен и чем выше будет подниматься, тем больше будет затемнение. В shortFormHeight прозрачность равна 0.0, в longFormHeight - 1.0.
|
||||
- transparent: dimmedView полностью прозрачен и будет пропускать все жесты на нижний (показывающий) контроллер
|
||||
- transparentWithShadow(_) dimmedView полностью прозрачен, однако модальное окно будет отбразывать тень на нижний контроллер. Все жесты так же проходят
|
||||
|
||||
> `UIViewShadow` получил статичное свойство для быстрой настройки тени котроллера
|
||||
|
||||
```swift
|
||||
class ShadowViewController: BaseModalViewController<UIView, UIView> {
|
||||
var dimmedViewType: DimmedView.AppearanceType {
|
||||
.transparentWithShadow(.defaultModalViewShadow)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Контроль закрытия
|
||||
|
||||
`PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`.
|
||||
|
||||
# Взаимедействие с PanModal
|
||||
|
||||
Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола:
|
||||
|
||||
- `DragView` больше не добавляется самостоятельно автоматически. Однако теперь к нему есть доступ, так что его даже можно как угодно настроить
|
||||
- Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss`
|
||||
- Больше нет свойст отвечающих за разрешение закрыть модалку тапом или свайпом. Если какое-то из этих действий необходимо запретить, объявите соответствующее действие (`onTapToDismiss` и `onDragToDismiss`) с nil
|
||||
- Можно настроить промежуточное состояние модального окна с `mediumFormHeight`
|
||||
- `DimmedView` остается настраиваемым с помощью `dimmedViewType` свойства
|
||||
|
||||
Через протокол `PanModalPresentable` у вас остается доступ к `presentationDetents`. Однако его установка никак не повлияет на настройку высоты и положений модального окна.
|
||||
|
||||
> Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.
|
||||
Loading…
Reference in New Issue