@@ -11,6 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate | |||
) -> Bool { | |||
window = UIWindow(frame: UIScreen.main.bounds) | |||
let vc = UIViewController() | |||
addSwiftUIViewAsChild(swiftUIView: V(), parent: vc.view) | |||
vc.view.backgroundColor = .gray | |||
window?.rootViewController = vc | |||
window?.backgroundColor = UIColor.white | |||
@@ -19,3 +20,132 @@ class AppDelegate: UIResponder, UIApplicationDelegate | |||
return true | |||
} | |||
} | |||
/* | |||
// Bus.Pipe.swift | |||
public extension Bus { | |||
final class BindingPipe<T: Equatable>: ObservableObject { | |||
public let input = PassthroughSubject<T, Never>() | |||
public let output = PassthroughSubject<T, Never>() | |||
@Published var value: T | |||
var isInput = false | |||
var subscriptions = [AnyCancellable]() | |||
deinit { | |||
print("ИГР BusBP.DEinit(\(Unmanaged.passUnretained(self).toOpaque()))") | |||
} | |||
init( | |||
_ defaultValue: T, | |||
_ keyApp: String, | |||
_ keyUI: String | |||
) { | |||
value = defaultValue | |||
/**/dbg("ИГР BusBP.init(\(Unmanaged.passUnretained(self).toOpaque())) keyA: '\(keyApp)'") | |||
// TextField -> output. | |||
$value | |||
.sink { [weak self] v in | |||
assert(Thread.isMainThread) | |||
// Ignore values received from input. | |||
// Only accept UI provided values. | |||
guard | |||
let self, | |||
!self.isInput | |||
else { | |||
return | |||
} | |||
self.output.send(v) | |||
} | |||
.store(in: &subscriptions) | |||
// input -> TextField. | |||
input | |||
.sink { [weak self] v in | |||
assert(Thread.isMainThread) | |||
guard let self else { return } | |||
/**/dbg("ИГР BusBP.init input: '\(v)'") | |||
self.isInput = true | |||
self.value = v | |||
self.isInput = false | |||
} | |||
.store(in: &subscriptions) | |||
// Оповещаем в мир об изменениях от UI. | |||
Bus.sendAsync( | |||
&subscriptions, | |||
keyUI, | |||
output.eraseToAnyPublisher() | |||
) | |||
Bus.receive( | |||
&subscriptions, | |||
[keyApp], | |||
{ [weak self] _, v in | |||
self?.input.send(v) | |||
/**/print("ИГР BusBP.init receive: '\(v)'") | |||
} | |||
) | |||
} | |||
} | |||
} | |||
// BusSlot.swift | |||
var busHolder = Bus.Service() | |||
// ======================================== | |||
// MARK: - MeetupX module | |||
// ======================================== | |||
enum MeetupId { | |||
enum Keys: String, RawRepresentable { | |||
case meetupIdTextApp | |||
case meetupIdTextUI | |||
} | |||
static func shouldFormat(_ s: String) -> String? { | |||
return s.components(separatedBy: NSCharacterSet.decimalDigits.inverted).reduce("") { $0 + $1 } | |||
} | |||
final class MeetupIdFormatter { | |||
var subscriptions = [AnyCancellable]() | |||
deinit { | |||
/**/dbg("ИГР MeetupIF.DEinit") | |||
} | |||
init() { | |||
Bus.receive( | |||
&subscriptions, | |||
[Keys.meetupIdTextUI.rawValue], | |||
{ [weak self] k, v in self?.handleFormatting(k, v) } | |||
) | |||
/**/dbg("ИГР MeetupIF.init") | |||
} | |||
func handleFormatting(_: String, _ value: String) { | |||
let out = MeetupId.shouldFormat(value) | |||
/**/dbg("ИГР MeetupIF.handleF out/dt: '\(out)'/'\(Date())'") | |||
Bus.Service.singleton?.send(Keys.meetupIdTextApp.rawValue, out) | |||
} | |||
} | |||
struct V: View { | |||
/*@StateObject*/ var fmt = MeetupIdFormatter() | |||
@StateObject var txt = Bus.BindingPipe("", Keys.meetupIdTextApp.rawValue, Keys.meetupIdTextUI.rawValue) | |||
var body: some View { | |||
VStack { | |||
Text("Hi, the text is: '\(txt.value)'") | |||
.fontWeight(.bold) | |||
TextField("Placeholder", text: $txt.value) | |||
.padding(8) | |||
.border(Color.blue, width: 2) | |||
} | |||
.frame(width: 320) | |||
.padding() | |||
} | |||
} | |||
} | |||
*/ |
@@ -0,0 +1,55 @@ | |||
import Combine | |||
import SwiftUI | |||
public enum Bus { } | |||
extension Bus { | |||
public final class Service { | |||
static private(set) weak var singleton: Service? | |||
let broadcaster = PassthroughSubject<(key: String, value: Any), Never>() | |||
deinit { | |||
Self.singleton = nil | |||
} | |||
public init() { | |||
Self.singleton = self | |||
} | |||
func send(_ key: String, _ value: Any) { | |||
broadcaster.send((key, value)) | |||
} | |||
} | |||
} | |||
public extension Bus { | |||
static func receive<T>( | |||
_ subscriptions: inout Set<AnyCancellable>, | |||
_ keys: Set<String>, | |||
_ handler: @escaping ((String, T) -> Void) | |||
) { | |||
Service.singleton?.broadcaster | |||
.compactMap { v -> (String, T)? in | |||
guard | |||
keys.contains(v.key), | |||
let value = v.value as? T | |||
else { | |||
return nil | |||
} | |||
return (v.key, value) | |||
} | |||
.sink { v in handler(v.0, v.1) } | |||
.store(in: &subscriptions) | |||
} | |||
static func sendAsync<T: Equatable>( | |||
_ subscriptions: inout Set<AnyCancellable>, | |||
_ key: String, | |||
_ node: AnyPublisher<T, Never> | |||
) { | |||
node | |||
.receive(on: DispatchQueue.main) | |||
.sink { v in Service.singleton?.send(key, v) } | |||
.store(in: &subscriptions) | |||
} | |||
} |
@@ -0,0 +1,20 @@ | |||
import SwiftUI | |||
struct V: View { | |||
@StateObject var vm = VM() | |||
var body: some View { | |||
VStack { | |||
HStack { | |||
Text("Check text field:") | |||
Text("'\(vm.text)'") | |||
.fontWeight(.bold) | |||
} | |||
TextField("Placeholder", text: $vm.text) | |||
.padding(8) | |||
.border(Color.red, width: 2) | |||
} | |||
.frame(width: 320) | |||
.padding() | |||
} | |||
} |
@@ -0,0 +1,38 @@ | |||
import Combine | |||
import SwiftUI | |||
final class VM: ObservableObject { | |||
@Published var text = "" | |||
let changeText = PassthroughSubject<String, Never>() | |||
var subscriptions = Set<AnyCancellable>() | |||
init() { | |||
// Уведомляем об изменении текста. | |||
$text | |||
.sink { [weak self] v in self?.changeText.send(v) } | |||
.store(in: &subscriptions) | |||
// Интерпретируем текст с задержкой, потому что: | |||
// 1. смена @Published в ту же секунду игнорируется | |||
// полем ввода, т.е. нужна задержка | |||
// 2. ожидаем окончания ввода (спама), чтобы | |||
// обработать введённое без потерь из-за конфликта | |||
// старых данных и новых. | |||
changeText | |||
.debounce(for: .seconds(0.3), scheduler: DispatchQueue.main) | |||
.sink { [weak self] v in | |||
/* | |||
guard | |||
let out = MeetupId.shouldFormat(v), | |||
v != out | |||
else { | |||
return | |||
} | |||
*/ | |||
let out = v | |||
/**/print("ИГР TFCVM.init changeT in/out: '\(v)'/'\(out)'") | |||
self?.text = out | |||
} | |||
.store(in: &subscriptions) | |||
} | |||
} |
@@ -0,0 +1,19 @@ | |||
import SwiftUI | |||
@discardableResult | |||
public func addSwiftUIViewAsChild<Type>( | |||
swiftUIView: Type, | |||
parent: UIView | |||
) -> UIHostingController<Type> { | |||
let childVC = UIHostingController(rootView: swiftUIView) | |||
childVC.view.backgroundColor = .clear | |||
parent.addSubview(childVC.view) | |||
childVC.view.translatesAutoresizingMaskIntoConstraints = false | |||
NSLayoutConstraint.activate([ | |||
childVC.view.topAnchor.constraint(equalTo: parent.topAnchor), | |||
childVC.view.bottomAnchor.constraint(equalTo: parent.bottomAnchor), | |||
childVC.view.leadingAnchor.constraint(equalTo: parent.leadingAnchor), | |||
childVC.view.trailingAnchor.constraint(equalTo: parent.trailingAnchor), | |||
]) | |||
return childVC | |||
} |