diff --git a/Swiftgram/Playground/BUILD b/Swiftgram/Playground/BUILD index fecebb3c58..181edbce77 100644 --- a/Swiftgram/Playground/BUILD +++ b/Swiftgram/Playground/BUILD @@ -6,7 +6,7 @@ load( "xcodeproj", ) load( - "//Swiftgram/Playground:custom_bazel_path.bzl", "custom_bazel_path" + "@build_configuration//:variables.bzl", "telegram_bazel_path" ) objc_library( @@ -24,6 +24,13 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/LegacyUI:LegacyUI", + "//submodules/LegacyComponents:LegacyComponents", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//Swiftgram/SGSwiftUI:SGSwiftUI", + ], + data = [ + "//Telegram:GeneratedPresentationStrings/Resources/PresentationStrings.data", ], visibility = ["//visibility:public"], ) @@ -38,12 +45,15 @@ ios_application( infoplists = ["Resources/Info.plist"], minimum_os_version = "14.0", visibility = ["//visibility:public"], + strings = [ + "//Telegram:AppStringResources", + ], launch_storyboard = "Resources/LaunchScreen.storyboard", deps = [":PlaygroundMain", ":PlaygroundLib"], ) xcodeproj( - bazel_path = custom_bazel_path(), + bazel_path = telegram_bazel_path, name = "Playground_xcodeproj", build_mode = "bazel", project_name = "Playground", diff --git a/Swiftgram/Playground/README.md b/Swiftgram/Playground/README.md index 59cee5b336..cdb7a64699 100644 --- a/Swiftgram/Playground/README.md +++ b/Swiftgram/Playground/README.md @@ -4,17 +4,7 @@ Small app to quickly iterate on components testing without building an entire me ## Generate Xcode project -### From root - -```shell -./Swiftgram/Playground/generate_project.py -``` - -### From current directory - -```shell -./generate_project.py -``` +Same as main project described in [../../Readme.md](../../Readme.md), but with `--target="Swiftgram/Playground"` parameter. ## Run generated project on simulator diff --git a/Swiftgram/Playground/Sources/AppDelegate.swift b/Swiftgram/Playground/Sources/AppDelegate.swift index 68beb2d5bd..69404da227 100644 --- a/Swiftgram/Playground/Sources/AppDelegate.swift +++ b/Swiftgram/Playground/Sources/AppDelegate.swift @@ -2,7 +2,9 @@ import UIKit import SwiftUI import AsyncDisplayKit import Display +import LegacyUI +let SHOW_SAFE_AREA = false @objc(AppDelegate) final class AppDelegate: NSObject, UIApplicationDelegate { @@ -18,7 +20,6 @@ final class AppDelegate: NSObject, UIApplicationDelegate { hostView.containerView.backgroundColor = UIColor.white self.window = window - let navigationController = NavigationController( mode: .single, theme: NavigationControllerTheme( @@ -30,7 +31,49 @@ final class AppDelegate: NSObject, UIApplicationDelegate { mainWindow.viewController = navigationController - navigationController.setViewControllers([mySwiftUIViewController(0)], animated: false) + let rootViewController = mySwiftUIViewController(0) + + if SHOW_SAFE_AREA { + // Add insets visualization + rootViewController.view.layoutMargins = .zero + rootViewController.view.subviews.forEach { $0.removeFromSuperview() } + + let topInsetView = UIView() + let leftInsetView = UIView() + let rightInsetView = UIView() + let bottomInsetView = UIView() + + [topInsetView, leftInsetView, rightInsetView, bottomInsetView].forEach { + $0.backgroundColor = .systemRed + $0.alpha = 0.3 + rootViewController.view.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + NSLayoutConstraint.activate([ + topInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor), + topInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor), + topInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor), + topInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.topAnchor), + + leftInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor), + leftInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor), + leftInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor), + leftInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.leadingAnchor), + + rightInsetView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor), + rightInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor), + rightInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor), + rightInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.trailingAnchor), + + bottomInsetView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor), + bottomInsetView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor), + bottomInsetView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor), + bottomInsetView.topAnchor.constraint(equalTo: rootViewController.view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + navigationController.setViewControllers([rootViewController], animated: false) self.window?.makeKeyAndVisible() diff --git a/Swiftgram/Playground/Sources/SwiftUIViewController.swift b/Swiftgram/Playground/Sources/SwiftUIViewController.swift index f4963692f0..139230a38a 100644 --- a/Swiftgram/Playground/Sources/SwiftUIViewController.swift +++ b/Swiftgram/Playground/Sources/SwiftUIViewController.swift @@ -1,256 +1,85 @@ import AsyncDisplayKit import Display import Foundation +import LegacyUI +import SGSwiftUI import SwiftUI +import TelegramPresentationData import UIKit -public class SwiftUIViewControllerInteraction { - let push: (ViewController) -> Void - let present: ( - _ controller: ViewController, - _ in: PresentationContextType, - _ with: ViewControllerPresentationArguments? - ) -> Void - let dismiss: (_ animated: Bool, _ completion: (() -> Void)?) -> Void +struct MySwiftUIView: View { + weak var wrapperController: LegacyController? - init( - push: @escaping (ViewController) -> Void, - present: @escaping ( - _ controller: ViewController, - _ in: PresentationContextType, - _ with: ViewControllerPresentationArguments? - ) -> Void, - dismiss: @escaping (_ animated: Bool, _ completion: (() -> Void)?) -> Void - ) { - self.push = push - self.present = present - self.dismiss = dismiss - } -} - -public protocol SwiftUIView: View { - var controllerInteraction: SwiftUIViewControllerInteraction? { get set } - var navigationHeight: CGFloat { get set } -} - -struct MySwiftUIView: SwiftUIView { - var controllerInteraction: SwiftUIViewControllerInteraction? - @Binding var navigationHeight: CGFloat - - var num: Int64 var body: some View { - Color.orange - .padding(.top, 2.0 * (_navigationHeight ?? 0)) + ScrollView { + Text("Hello, World!") + .font(.title) + .foregroundColor(.black) + + Spacer(minLength: 0) + + Button("Push") { + self.wrapperController?.push(mySwiftUIViewController(num + 1)) + }.buttonStyle(AppleButtonStyle()) + Spacer() + Button("Modal") { + self.wrapperController?.present( + mySwiftUIViewController(num + 1), + in: .window(.root), + with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet) + ) + }.buttonStyle(AppleButtonStyle()) + Spacer() + if num > 0 { + Button("Dismiss") { + self.wrapperController?.dismiss() + }.buttonStyle(AppleButtonStyle()) + Spacer() + } + ForEach(1..<20, id: \.self) { i in + Button("TAP: \(i)") { + print("Tapped \(i)") + }.buttonStyle(AppleButtonStyle()) + } + + } + .background(Color.green) } } -struct CustomButtonStyle: ButtonStyle { +struct AppleButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label - .padding() - .background(Color.blue) + .font(.headline) .foregroundColor(.white) - .cornerRadius(8) - .frame(height: 44) // Set a fixed height for all buttons + .padding() + .frame(minWidth: 0, maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(10) + .scaleEffect(configuration.isPressed ? 0.95 : 1) + .opacity(configuration.isPressed ? 0.9 : 1) } } -private final class SwiftUIViewControllerNode: ASDisplayNode { - private let hostingController: UIHostingController - private var isDismissed = false - private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? +public func mySwiftUIViewController(_ num: Int64) -> ViewController { + let legacyController = LegacySwiftUIController( + presentation: .modal(animateIn: true), + theme: defaultPresentationTheme, + strings: defaultPresentationStrings + ) + legacyController.statusBar.statusBarStyle = defaultPresentationTheme.rootController + .statusBarStyle.style + legacyController.title = "Controller: \(num)" - init(swiftUIView: Content) { - self.hostingController = UIHostingController(rootView: swiftUIView) - super.init() - - // For debugging - self.backgroundColor = .red.withAlphaComponent(0.3) - hostingController.view.backgroundColor = .blue.withAlphaComponent(0.3) - } + let swiftUIView = SGSwiftUIView( + navigationBarHeight: legacyController.navigationBarHeightModel, + containerViewLayout: legacyController.containerViewLayoutModel, + content: { MySwiftUIView(wrapperController: legacyController, num: num) } + ) + let controller = UIHostingController(rootView: swiftUIView, ignoreSafeArea: true) + legacyController.bind(controller: controller) - override func didLoad() { - super.didLoad() - - // Defer the setup to ensure we have a valid view controller hierarchy - DispatchQueue.main.async { [weak self] in - self?.setupHostingController() - } - } - - private func setupHostingController() { - guard let viewController = findViewController() else { - assert(true, "Error: Could not find a parent view controller") - return - } - - viewController.addChild(hostingController) - view.addSubview(hostingController.view) - hostingController.didMove(toParent: viewController) - - // Ensure the hosting controller's view has a size - hostingController.view.frame = self.bounds - - print("SwiftUIViewControllerNode setup - Node frame: \(self.frame), Hosting view frame: \(hostingController.view.frame)") - } - - private func findViewController() -> UIViewController? { - var responder: UIResponder? = self.view - while let nextResponder = responder?.next { - if let viewController = nextResponder as? UIViewController { - return viewController - } - responder = nextResponder - } - return nil - } - - override func layout() { - super.layout() - hostingController.view.frame = self.bounds - print("SwiftUIViewControllerNode layout - Node frame: \(self.frame), Hosting view frame: \(hostingController.view.frame)") - } - - func containerLayoutUpdated( - layout: ContainerViewLayout, - navigationHeight: CGFloat, - transition: ContainedViewLayoutTransition - ) { - if self.isDismissed { - return - } - - self.validLayout = (layout, navigationHeight) - - let frame = CGRect( - origin: CGPoint(x: 0, y: 0), - size: CGSize( - width: layout.size.width, - height: layout.size.height - ) - ) - - transition.updateFrame(node: self, frame: frame) - - print("containerLayoutUpdated - New frame: \(frame)") - - // Ensure hosting controller view is updated - hostingController.view.frame = bounds - hostingController.rootView.navigationHeight = navigationHeight - } - - func animateOut(completion: @escaping () -> Void) { - guard let (layout, navigationHeight) = validLayout else { - completion() - return - } - self.isDismissed = true - let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - - let frame = CGRect( - origin: CGPoint(x: 0, y: 0), - size: CGSize( - width: layout.size.width, - height: layout.size.height - ) - ) - - transition.updateFrame(node: self, frame: frame, completion: { _ in - completion() - }) - hostingController.rootView.navigationHeight = navigationHeight - } - - override func didEnterHierarchy() { - super.didEnterHierarchy() - print("SwiftUIViewControllerNode entered hierarchy") - } - - override func didExitHierarchy() { - super.didExitHierarchy() - hostingController.willMove(toParent: nil) - hostingController.view.removeFromSuperview() - hostingController.removeFromParent() - print("SwiftUIViewControllerNode exited hierarchy") - } -} - -public final class SwiftUIViewController: ViewController { - private var swiftUIView: Content - - public init( - _ swiftUIView: Content, - navigationBarTheme: NavigationBarTheme = NavigationBarTheme( - buttonColor: ACCENT_COLOR, - disabledButtonColor: .gray, - primaryTextColor: .black, - backgroundColor: .clear, - enableBackgroundBlur: true, - separatorColor: .gray, - badgeBackgroundColor: THEME.navigationBar.badgeBackgroundColor, - badgeStrokeColor: THEME.navigationBar.badgeStrokeColor, - badgeTextColor: THEME.navigationBar.badgeTextColor - ), - navigationBarStrings: NavigationBarStrings = NavigationBarStrings( - back: "Back", - close: "Close" - ) - ) { - self.swiftUIView = swiftUIView - super.init(navigationBarPresentationData: NavigationBarPresentationData( - theme: navigationBarTheme, - strings: navigationBarStrings - )) - - self.swiftUIView.controllerInteraction = SwiftUIViewControllerInteraction( - push: { [weak self] c in - guard let strongSelf = self else { return } - strongSelf.push(c) - }, - present: { [weak self] c, context, args in - guard let strongSelf = self else { return } - strongSelf.present(c, in: context, with: args) - }, - dismiss: { [weak self] animated, completion in - guard let strongSelf = self else { return } - strongSelf.dismiss(animated: animated, completion: completion) - } - ) - } - - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func loadDisplayNode() { - self.displayNode = SwiftUIViewControllerNode(swiftUIView: swiftUIView) - } - - override public func containerLayoutUpdated( - _ layout: ContainerViewLayout, - transition: ContainedViewLayoutTransition - ) { - super.containerLayoutUpdated(layout, transition: transition) - - (self.displayNode as! SwiftUIViewControllerNode).containerLayoutUpdated( - layout: layout, - navigationHeight: navigationLayout(layout: layout).navigationFrame.maxY, - transition: transition - ) - } - - public func animateOut(completion: @escaping () -> Void) { - (self.displayNode as! SwiftUIViewControllerNode) - .animateOut(completion: completion) - } -} - - -func mySwiftUIViewController(_ num: Int64) -> ViewController { - let controller = SwiftUIViewController(MySwiftUIView(num: num)) - controller.title = "Controller: \(num)" - return controller + return legacyController } diff --git a/Swiftgram/SGSwiftUI/BUILD b/Swiftgram/SGSwiftUI/BUILD new file mode 100644 index 0000000000..9437ba2d57 --- /dev/null +++ b/Swiftgram/SGSwiftUI/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SGSwiftUI", + module_name = "SGSwiftUI", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + # "-warnings-as-errors", + ], + deps = [ + "//submodules/LegacyUI:LegacyUI", + "//submodules/Display:Display", + "//submodules/TelegramPresentationData:TelegramPresentationData" + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift b/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift new file mode 100644 index 0000000000..7db1a3ecd7 --- /dev/null +++ b/Swiftgram/SGSwiftUI/Sources/SGSwiftUI.swift @@ -0,0 +1,145 @@ +import Display +import Foundation +import LegacyUI +import SwiftUI +import TelegramPresentationData + +public class ObservedValue: ObservableObject { + @Published var value: T + + init(_ value: T) { + self.value = value + } +} + +public struct SGSwiftUIView: View { + let content: Content + + @ObservedObject var navigationBarHeight: ObservedValue + @ObservedObject var containerViewLayout: ObservedValue + + public init( + navigationBarHeight: ObservedValue, + containerViewLayout: ObservedValue, + @ViewBuilder content: () -> Content + ) { + self.navigationBarHeight = navigationBarHeight + self.containerViewLayout = containerViewLayout + self.content = content() + } + + public var body: some View { + content + .modifier(CustomSafeAreaPadding(navigationBarHeight: navigationBarHeight, containerViewLayout: containerViewLayout)) + .background(Color.yellow) + } +} + +public struct CustomSafeAreaPadding: ViewModifier { + @ObservedObject var navigationBarHeight: ObservedValue + @ObservedObject var containerViewLayout: ObservedValue + + public func body(content: Content) -> some View { + content + .edgesIgnoringSafeArea(.all) + .padding(.top, totalTopSafeArea > navigationBarHeight.value ? totalTopSafeArea : navigationBarHeight.value) + .padding(.bottom, (containerViewLayout.value?.safeInsets.bottom ?? 0) + (containerViewLayout.value?.intrinsicInsets.bottom ?? 0)) + .padding(.leading, containerViewLayout.value?.safeInsets.left ?? 0) + .padding(.trailing, containerViewLayout.value?.safeInsets.right ?? 0) + } + + var totalTopSafeArea: CGFloat { + (containerViewLayout.value?.safeInsets.top ?? 0) + + (containerViewLayout.value?.intrinsicInsets.top ?? 0) + } +} + +public final class LegacySwiftUIController: LegacyController { + public var navigationBarHeightModel: ObservedValue + public var containerViewLayoutModel: ObservedValue + + override public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) { + navigationBarHeightModel = ObservedValue(0.0) + containerViewLayoutModel = ObservedValue(initialLayout) + super.init(presentation: presentation, theme: theme, strings: strings, initialLayout: initialLayout) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + let newNavigationBarHeight = navigationLayout(layout: layout).navigationFrame.maxY + if navigationBarHeightModel.value != newNavigationBarHeight { + navigationBarHeightModel.value = newNavigationBarHeight + } + if containerViewLayoutModel.value != layout { + containerViewLayoutModel.value = layout + } + } + + override public func bind(controller: UIViewController) { + super.bind(controller: controller) + addChild(legacyController) + legacyController.didMove(toParent: legacyController) + } + + @available(*, unavailable) + public required init(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension UIHostingController { + public convenience init(rootView: Content, ignoreSafeArea: Bool) { + self.init(rootView: rootView) + + if ignoreSafeArea { + disableSafeArea() + } + } + + func disableSafeArea() { + guard let viewClass = object_getClass(view) else { + return + } + + func encodeText(string: String, key: Int16) -> String { + let nsString = string as NSString + let result = NSMutableString() + for i in 0 ..< nsString.length { + var c: unichar = nsString.character(at: i) + c = unichar(Int16(c) + key) + result.append(NSString(characters: &c, length: 1) as String) + } + return result as String + } + + let viewSubclassName = String(cString: class_getName(viewClass)).appending(encodeText(string: "`JhopsfTbgfBsfb", key: -1)) + + if let viewSubclass = NSClassFromString(viewSubclassName) { + object_setClass(view, viewSubclass) + } else { + guard + let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String, + let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) + else { + return + } + + if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) { + let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in + .zero + } + + class_addMethod( + viewSubclass, + #selector(getter: UIView.safeAreaInsets), + imp_implementationWithBlock(safeAreaInsets), + method_getTypeEncoding(method) + ) + } + + objc_registerClassPair(viewSubclass) + object_setClass(view, viewSubclass) + } + } +} diff --git a/Telegram/BUILD b/Telegram/BUILD index 32cf497229..a4b8f9bd08 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -138,6 +138,10 @@ genrule( "GeneratedPresentationStrings/Sources/PresentationStrings.m", "GeneratedPresentationStrings/Resources/PresentationStrings.data", ], + # MARK: Swiftgram + visibility = [ + "//visibility:public", + ], ) minimum_os_version = "12.0" @@ -253,7 +257,9 @@ filegroup( "//Swiftgram/SGStrings:SGLocalizableStrings", ] + [ "{}.lproj/Localizable.strings".format(language) for language in empty_languages - ] + ], + # MARK: Swiftgram + visibility = ["//visibility:public",], ) filegroup( diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py index 953f4caa4c..4f8fe95d8c 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -34,6 +34,9 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, project_bazel_arguments.append(argument) project_bazel_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)] + if target_name == "Swiftgram/Playground": + project_bazel_arguments += ["--swiftcopt=-no-warnings-as-errors", "--copt=-Wno-error"]#, "--swiftcopt=-DSWIFTGRAM_PLAYGROUND", "--copt=-DSWIFTGRAM_PLAYGROUND=1"] + if target_name == 'Telegram': if disable_extensions: project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)] @@ -51,6 +54,10 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, call_executable(bazel_generate_arguments) # MARK: Swiftgram + if target_name == "Swiftgram/Playground": + xcodeproj_path = 'Swiftgram/Playground/Playground.xcodeproj' + call_executable(['open', xcodeproj_path]) + return xcodeproj_path = 'Telegram/Swiftgram.xcodeproj' call_executable(['open', xcodeproj_path]) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaVideoConverter.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaVideoConverter.h index e110fed1a7..3686961190 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaVideoConverter.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaVideoConverter.h @@ -2,6 +2,11 @@ #import +// MARK: Swiftgram +#import +#import +// + @interface TGMediaVideoFileWatcher : NSObject { NSURL *_fileURL; diff --git a/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index 11257c2b60..1aa02d04f8 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -468,7 +468,7 @@ open class LegacyController: ViewController, PresentableController { fatalError("init(coder:) has not been implemented") } - public func bind(controller: UIViewController) { + open func bind(controller: UIViewController) { self.legacyController = controller if let controller = controller as? TGViewController { controller.customRemoveFromParentViewController = { [weak self] in