SGSwiftUI lib to embed views into Telegram's stack

This commit is contained in:
Kylmakalle 2024-10-01 01:21:25 +03:00
parent bb6c04e52d
commit a9dfb01961
5 changed files with 247 additions and 102 deletions

View File

@ -27,6 +27,7 @@ swift_library(
"//submodules/LegacyUI:LegacyUI",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/MediaPlayer:UniversalMediaPlayer",
"//Swiftgram/SGSwiftUI:SGSwiftUI",
],
data = [
"//Telegram:GeneratedPresentationStrings/Resources/PresentationStrings.data",

View File

@ -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<MySwiftUIView>(
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
}

20
Swiftgram/SGSwiftUI/BUILD Normal file
View File

@ -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",
],
)

View File

@ -0,0 +1,145 @@
import Display
import Foundation
import LegacyUI
import SwiftUI
import TelegramPresentationData
public class ObservedValue<T>: ObservableObject {
@Published var value: T
init(_ value: T) {
self.value = value
}
}
public struct SGSwiftUIView<Content: View>: View {
let content: Content
@ObservedObject var navigationBarHeight: ObservedValue<CGFloat>
@ObservedObject var containerViewLayout: ObservedValue<ContainerViewLayout?>
public init(
navigationBarHeight: ObservedValue<CGFloat>,
containerViewLayout: ObservedValue<ContainerViewLayout?>,
@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<CGFloat>
@ObservedObject var containerViewLayout: ObservedValue<ContainerViewLayout?>
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<CGFloat>
public var containerViewLayoutModel: ObservedValue<ContainerViewLayout?>
override public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) {
navigationBarHeightModel = ObservedValue<CGFloat>(0.0)
containerViewLayoutModel = ObservedValue<ContainerViewLayout?>(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)
}
}
}

View File

@ -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
}
}