diff --git a/Swiftgram/Playground/BUILD b/Swiftgram/Playground/BUILD index 87aaa2b6be..181edbce77 100644 --- a/Swiftgram/Playground/BUILD +++ b/Swiftgram/Playground/BUILD @@ -27,6 +27,7 @@ swift_library( "//submodules/LegacyUI:LegacyUI", "//submodules/LegacyComponents:LegacyComponents", "//submodules/MediaPlayer:UniversalMediaPlayer", + "//Swiftgram/SGSwiftUI:SGSwiftUI", ], data = [ "//Telegram:GeneratedPresentationStrings/Resources/PresentationStrings.data", diff --git a/Swiftgram/Playground/Sources/SwiftUIViewController.swift b/Swiftgram/Playground/Sources/SwiftUIViewController.swift index 31a5a6fb12..139230a38a 100644 --- a/Swiftgram/Playground/Sources/SwiftUIViewController.swift +++ b/Swiftgram/Playground/Sources/SwiftUIViewController.swift @@ -1,9 +1,85 @@ import AsyncDisplayKit import Display import Foundation -import SwiftUI -import UIKit import LegacyUI +import SGSwiftUI +import SwiftUI import TelegramPresentationData +import UIKit +struct MySwiftUIView: View { + weak var wrapperController: LegacyController? + var num: Int64 + + var body: some View { + 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 AppleButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.headline) + .foregroundColor(.white) + .padding() + .frame(minWidth: 0, maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(10) + .scaleEffect(configuration.isPressed ? 0.95 : 1) + .opacity(configuration.isPressed ? 0.9 : 1) + } +} + +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)" + + 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) + + 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/submodules/LegacyUI/Sources/LegacyController.swift b/submodules/LegacyUI/Sources/LegacyController.swift index 144281ee2c..1aa02d04f8 100644 --- a/submodules/LegacyUI/Sources/LegacyController.swift +++ b/submodules/LegacyUI/Sources/LegacyController.swift @@ -4,7 +4,6 @@ import Display import SwiftSignalKit import LegacyComponents import TelegramPresentationData -import SwiftUI public enum LegacyControllerPresentation { case custom @@ -438,9 +437,6 @@ open class LegacyController: ViewController, PresentableController { } } - // MARK: Swiftgram - public var navigationBarHeightModel: ObservedNavigationBarHeight - public var disposables = DisposableSet() public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) { @@ -454,7 +450,6 @@ open class LegacyController: ViewController, PresentableController { } else { navigationBarPresentationData = nil } - self.navigationBarHeightModel = ObservedNavigationBarHeight(value: 0.0) super.init(navigationBarPresentationData: navigationBarPresentationData) if let theme = theme { @@ -473,17 +468,13 @@ 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 self?.dismiss() } } - if self.legacyController.isHosting { - self.addChild(self.legacyController) - self.legacyController.didMove(toParent: legacyController) - } } override open func loadDisplayNode() { @@ -593,8 +584,8 @@ open class LegacyController: ViewController, PresentableController { self.validLayout = layout super.containerLayoutUpdated(layout, transition: transition) - let navigationBarHeight = self.navigationLayout(layout: layout).navigationFrame.maxY - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) if let legacyTelegramController = self.legacyController as? TGViewController { var duration: TimeInterval = 0.0 if case let .animated(transitionDuration, _) = transition { @@ -623,7 +614,6 @@ open class LegacyController: ViewController, PresentableController { if previousSizeClass != updatedSizeClass { self.sizeClass.set(SSignal.single(updatedSizeClass.rawValue as NSNumber)) } - self.navigationBarHeightModel.value = navigationBarHeight } override open func dismiss(completion: (() -> Void)? = nil) { @@ -646,90 +636,3 @@ open class LegacyController: ViewController, PresentableController { } } } - -// MARK: Swiftgram -private protocol AnyUIHostingViewController: AnyObject {} -extension UIHostingController: AnyUIHostingViewController {} - -extension UIViewController { - var isHosting: Bool { self is AnyUIHostingViewController } -} - - - - -struct MySwiftUIView: View { - weak var wrapperController: LegacyController? - - var num: Int64 - - @ObservedObject var navigationBarHeight: ObservedNavigationBarHeight - - var body: some View { - VStack { - 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() - } - } - .background(Color.green) - .padding(.top, self.navigationBarHeight.value) - } - -// func containerLayoutUpdatedNotification(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { -// print("[\(num)] SwiftUI container update with navigationBarHeight \(navigationBarHeight)") -// self.navigationBarHeightValue = navigationBarHeight -// } - -} - - -struct AppleButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.headline) - .foregroundColor(.white) - .padding() - .frame(minWidth: 0, maxWidth: .infinity) - .background(Color.blue) - .cornerRadius(10) - .scaleEffect(configuration.isPressed ? 0.95 : 1) - .opacity(configuration.isPressed ? 0.9 : 1) - } -} - -public func mySwiftUIViewController(_ num: Int64) -> ViewController { - let legacyController = LegacyController(presentation: .navigation, theme: defaultPresentationTheme, strings: defaultPresentationStrings) - legacyController.statusBar.statusBarStyle = defaultPresentationTheme.rootController.statusBarStyle.style - legacyController.title = "Controller: \(num)" - - let swiftUIView = MySwiftUIView(wrapperController: legacyController, num: num, navigationBarHeight: legacyController.navigationBarHeightModel) - let controller = UIHostingController(rootView: swiftUIView) - legacyController.bind(controller: controller) - - return legacyController -} - -public class ObservedNavigationBarHeight: ObservableObject { - @Published var value: CGFloat - - init(value: CGFloat) { - self.value = value - } -}