WIP Injecting SwiftUI via LegacyController

This commit is contained in:
Kylmakalle 2024-09-29 15:56:21 +03:00
parent 4b1edac9e2
commit f38e31dc22
7 changed files with 279 additions and 303 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,12 @@ swift_library(
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/LegacyUI:LegacyUI",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/MediaPlayer:UniversalMediaPlayer",
],
data = [
"//Telegram:GeneratedPresentationStrings/Resources/PresentationStrings.data",
],
visibility = ["//visibility:public"],
)
@ -38,12 +44,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

@ -3,6 +3,8 @@ import Display
import Foundation
import SwiftUI
import UIKit
import LegacyUI
import TelegramPresentationData
public class SwiftUIViewControllerInteraction {
let push: (ViewController) -> Void
@ -27,230 +29,265 @@ public class SwiftUIViewControllerInteraction {
self.dismiss = dismiss
}
}
//
//public protocol SwiftUIView: View {
// var controllerInteraction: SwiftUIViewControllerInteraction? { get set }
//}
//
//struct MySwiftUIView: SwiftUIView {
// var controllerInteraction: SwiftUIViewControllerInteraction?
//
//
// var num: Int64
//
// var body: some View {
// Color.orange
// }
//}
//
//struct CustomButtonStyle: ButtonStyle {
// func makeBody(configuration: Configuration) -> some View {
// configuration.label
// .padding()
// .background(Color.blue)
// .foregroundColor(.white)
// .cornerRadius(8)
// .frame(height: 44) // Set a fixed height for all buttons
// }
//}
//
//private final class SwiftUIViewControllerNode<Content: SwiftUIView>: ASDisplayNode {
// private let hostingController: UIHostingController<Content>
// private var isDismissed = false
// private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
//
// 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)
// }
//
// 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
// }
//
// 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()
// })
// }
//
// 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
//}
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))
// VStack {
// Button("Push") {
// self.controllerInteraction?.push(mySwiftUIViewController(num + 1))
// }
// Button("Modal") {
// self.controllerInteraction?.present(mySwiftUIViewController(num + 1), .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
// }
// Button("Dismiss") {
// self.controllerInteraction?.dismiss(true, {})
// }
// }
}
}
struct CustomButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
.frame(height: 44) // Set a fixed height for all buttons
}
}
private final class SwiftUIViewControllerNode<Content: SwiftUIView>: ASDisplayNode {
private let hostingController: UIHostingController<Content>
private var isDismissed = false
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
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)
}
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
let legacyController = LegacyController(presentation: .navigation, theme: defaultPresentationTheme, strings: defaultPresentationStrings)
legacyController.statusBar.statusBarStyle = defaultPresentationTheme.rootController.statusBarStyle.style
legacyController.title = "Controller: root"
let controller = UIHostingController(rootView: MySwiftUIView(num: num))
legacyController.bind(controller: controller)
legacyController.addChild(controller)
controller.didMove(toParent: legacyController)
return legacyController
}

View File

@ -1,78 +0,0 @@
#!/usr/bin/env python3
from contextlib import contextmanager
import os
import subprocess
import sys
import shutil
import textwrap
# Import the locate_bazel function
sys.path.append(
os.path.join(os.path.dirname(__file__), "..", "..", "build-system", "Make")
)
from BazelLocation import locate_bazel
@contextmanager
def cwd(path):
oldpwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(oldpwd)
def main():
# Get the current script directory
current_script_dir = os.path.dirname(os.path.abspath(__file__))
with cwd(os.path.join(current_script_dir, "..", "..")):
bazel_path = locate_bazel(os.getcwd())
# 1. Kill all Xcode processes
subprocess.run(["killall", "Xcode"], check=False)
# 2. Delete xcodeproj.bazelrc if it exists and write a new one
bazelrc_path = os.path.join(current_script_dir, "..", "..", "xcodeproj.bazelrc")
if os.path.exists(bazelrc_path):
os.remove(bazelrc_path)
with open(bazelrc_path, "w") as f:
f.write(
textwrap.dedent(
"""
build --announce_rc
build --features=swift.use_global_module_cache
build --verbose_failures
build --features=swift.enable_batch_mode
build --features=-swift.debug_prefix_map
# build --disk_cache=
build --swiftcopt=-no-warnings-as-errors
build --copt=-Wno-error
"""
)
)
# 3. Delete the Xcode project if it exists
xcode_project_path = os.path.join(current_script_dir, "Playground.xcodeproj")
if os.path.exists(xcode_project_path):
shutil.rmtree(xcode_project_path)
# 4. Write content to generate_project.py
generate_project_path = os.path.join(current_script_dir, "custom_bazel_path.bzl")
with open(generate_project_path, "w") as f:
f.write("def custom_bazel_path():\n")
f.write(f' return "{bazel_path}"\n')
# 5. Run xcodeproj generator
working_dir = os.path.join(current_script_dir, "..", "..")
bazel_command = f'"{bazel_path}" run //Swiftgram/Playground:Playground_xcodeproj'
subprocess.run(bazel_command, shell=True, cwd=working_dir, check=True)
# 5. Open Xcode project
subprocess.run(["open", xcode_project_path], check=True)
if __name__ == "__main__":
main()

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;