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/LegacyUI:LegacyUI",
"//submodules/LegacyComponents:LegacyComponents", "//submodules/LegacyComponents:LegacyComponents",
"//submodules/MediaPlayer:UniversalMediaPlayer", "//submodules/MediaPlayer:UniversalMediaPlayer",
"//Swiftgram/SGSwiftUI:SGSwiftUI",
], ],
data = [ data = [
"//Telegram:GeneratedPresentationStrings/Resources/PresentationStrings.data", "//Telegram:GeneratedPresentationStrings/Resources/PresentationStrings.data",

View File

@ -1,9 +1,85 @@
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Foundation import Foundation
import SwiftUI
import UIKit
import LegacyUI import LegacyUI
import SGSwiftUI
import SwiftUI
import TelegramPresentationData 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 SwiftSignalKit
import LegacyComponents import LegacyComponents
import TelegramPresentationData import TelegramPresentationData
import SwiftUI
public enum LegacyControllerPresentation { public enum LegacyControllerPresentation {
case custom case custom
@ -438,9 +437,6 @@ open class LegacyController: ViewController, PresentableController {
} }
} }
// MARK: Swiftgram
public var navigationBarHeightModel: ObservedNavigationBarHeight
public var disposables = DisposableSet() public var disposables = DisposableSet()
public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) { public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) {
@ -454,7 +450,6 @@ open class LegacyController: ViewController, PresentableController {
} else { } else {
navigationBarPresentationData = nil navigationBarPresentationData = nil
} }
self.navigationBarHeightModel = ObservedNavigationBarHeight(value: 0.0)
super.init(navigationBarPresentationData: navigationBarPresentationData) super.init(navigationBarPresentationData: navigationBarPresentationData)
if let theme = theme { if let theme = theme {
@ -473,17 +468,13 @@ open class LegacyController: ViewController, PresentableController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public func bind(controller: UIViewController) { open func bind(controller: UIViewController) {
self.legacyController = controller self.legacyController = controller
if let controller = controller as? TGViewController { if let controller = controller as? TGViewController {
controller.customRemoveFromParentViewController = { [weak self] in controller.customRemoveFromParentViewController = { [weak self] in
self?.dismiss() self?.dismiss()
} }
} }
if self.legacyController.isHosting {
self.addChild(self.legacyController)
self.legacyController.didMove(toParent: legacyController)
}
} }
override open func loadDisplayNode() { override open func loadDisplayNode() {
@ -593,8 +584,8 @@ open class LegacyController: ViewController, PresentableController {
self.validLayout = layout self.validLayout = layout
super.containerLayoutUpdated(layout, transition: transition) 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 { if let legacyTelegramController = self.legacyController as? TGViewController {
var duration: TimeInterval = 0.0 var duration: TimeInterval = 0.0
if case let .animated(transitionDuration, _) = transition { if case let .animated(transitionDuration, _) = transition {
@ -623,7 +614,6 @@ open class LegacyController: ViewController, PresentableController {
if previousSizeClass != updatedSizeClass { if previousSizeClass != updatedSizeClass {
self.sizeClass.set(SSignal.single(updatedSizeClass.rawValue as NSNumber)) self.sizeClass.set(SSignal.single(updatedSizeClass.rawValue as NSNumber))
} }
self.navigationBarHeightModel.value = navigationBarHeight
} }
override open func dismiss(completion: (() -> Void)? = nil) { 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
}
}