diff --git a/src/App.swift b/src/App.swift index 6167ae1..185ce53 100644 --- a/src/App.swift +++ b/src/App.swift @@ -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: ObservableObject { + public let input = PassthroughSubject() + public let output = PassthroughSubject() + @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() + } + } +} + +*/ diff --git a/src/Bus.swift b/src/Bus.swift new file mode 100644 index 0000000..4576fb9 --- /dev/null +++ b/src/Bus.swift @@ -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( + _ subscriptions: inout Set, + _ keys: Set, + _ 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( + _ subscriptions: inout Set, + _ key: String, + _ node: AnyPublisher + ) { + node + .receive(on: DispatchQueue.main) + .sink { v in Service.singleton?.send(key, v) } + .store(in: &subscriptions) + } +} diff --git a/src/V.swift b/src/V.swift new file mode 100644 index 0000000..4b6bc5f --- /dev/null +++ b/src/V.swift @@ -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() + } +} diff --git a/src/VM.swift b/src/VM.swift new file mode 100644 index 0000000..fd9730f --- /dev/null +++ b/src/VM.swift @@ -0,0 +1,38 @@ +import Combine +import SwiftUI + +final class VM: ObservableObject { + @Published var text = "" + let changeText = PassthroughSubject() + var subscriptions = Set() + + 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) + } +} diff --git a/src/addSwiftUIAsChild.swift b/src/addSwiftUIAsChild.swift new file mode 100644 index 0000000..14fdff2 --- /dev/null +++ b/src/addSwiftUIAsChild.swift @@ -0,0 +1,19 @@ +import SwiftUI + +@discardableResult +public func addSwiftUIViewAsChild( + swiftUIView: Type, + parent: UIView +) -> UIHostingController { + 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 +}