Independent Playground app for simulator

SGSwiftUI lib to embed views into Telegram's stack

ObservableObject

wip

WIP Injecting SwiftUI via LegacyController

New translations sglocalizable.strings (Chinese Traditional) (#57)

wip

Init SwiftUIViewController

Update .swiftformat

Move Playground to example

Create .swiftformat

Launch Playgound project on simulator

Inject SwiftUI view with overflow to AsyncDisplayKit

Playground UIKit base and controllers

WIP: Bump version

New translations sglocalizable.strings (Ukrainian) (#55)
This commit is contained in:
Kylmakalle
2024-09-26 00:35:09 +03:00
parent 1865c248ba
commit fe9a9c3e62
10 changed files with 306 additions and 251 deletions

View File

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

View File

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

View File

@@ -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()

View File

@@ -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<Content: SwiftUIView>: ASDisplayNode {
private let hostingController: UIHostingController<Content>
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<MySwiftUIView>(
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<Content: SwiftUIView>: 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<Content>(swiftUIView: swiftUIView)
}
override public func containerLayoutUpdated(
_ layout: ContainerViewLayout,
transition: ContainedViewLayoutTransition
) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! SwiftUIViewControllerNode<Content>).containerLayoutUpdated(
layout: layout,
navigationHeight: navigationLayout(layout: layout).navigationFrame.maxY,
transition: transition
)
}
public func animateOut(completion: @escaping () -> Void) {
(self.displayNode as! SwiftUIViewControllerNode<Content>)
.animateOut(completion: completion)
}
}
func mySwiftUIViewController(_ num: Int64) -> ViewController {
let controller = SwiftUIViewController(MySwiftUIView(num: num))
controller.title = "Controller: \(num)"
return 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

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

View File

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

View File

@@ -2,6 +2,11 @@
#import <LegacyComponents/TGVideoEditAdjustments.h>
// MARK: Swiftgram
#import <VideoToolbox/VideoToolbox.h>
#import <MediaPlayer/MediaPlayer.h>
//
@interface TGMediaVideoFileWatcher : NSObject
{
NSURL *_fileURL;

View File

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